From 4a1212095053bb93e936038a71f44b01ebbb3c36 Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Fri, 13 Jun 2025 16:27:48 -0400 Subject: [PATCH 01/47] [PM-19703] Fix admin count logic to exclude current organization (#5918) --- .../OrganizationUsersController.cs | 4 +- .../Public/Controllers/MembersController.cs | 3 +- .../IUpdateOrganizationUserCommand.cs | 3 +- .../UpdateOrganizationUserCommand.cs | 48 +++++++++++---- .../OrganizationUserControllerPutTests.cs | 48 ++++++++++----- .../UpdateOrganizationUserCommandTests.cs | 60 +++++++++++++++---- 6 files changed, 126 insertions(+), 40 deletions(-) diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 6b23edf347..1b58fcfebe 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -521,7 +521,9 @@ public class OrganizationUsersController : Controller .Concat(readonlyCollectionAccess) .ToList(); - await _updateOrganizationUserCommand.UpdateUserAsync(model.ToOrganizationUser(organizationUser), userId, + var existingUserType = organizationUser.Type; + + await _updateOrganizationUserCommand.UpdateUserAsync(model.ToOrganizationUser(organizationUser), existingUserType, userId, collectionsToSave, groupsToSave); } diff --git a/src/Api/AdminConsole/Public/Controllers/MembersController.cs b/src/Api/AdminConsole/Public/Controllers/MembersController.cs index 6552684ca3..90c78c9eb7 100644 --- a/src/Api/AdminConsole/Public/Controllers/MembersController.cs +++ b/src/Api/AdminConsole/Public/Controllers/MembersController.cs @@ -177,9 +177,10 @@ public class MembersController : Controller { return new NotFoundResult(); } + var existingUserType = existingUser.Type; var updatedUser = model.ToOrganizationUser(existingUser); var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection()).ToList(); - await _updateOrganizationUserCommand.UpdateUserAsync(updatedUser, null, associations, model.Groups); + await _updateOrganizationUserCommand.UpdateUserAsync(updatedUser, existingUserType, null, associations, model.Groups); MemberResponseModel response = null; if (existingUser.UserId.HasValue) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IUpdateOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IUpdateOrganizationUserCommand.cs index 0cd5a3295f..d1fc9fbbec 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IUpdateOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IUpdateOrganizationUserCommand.cs @@ -1,11 +1,12 @@ #nullable enable using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Models.Data; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; public interface IUpdateOrganizationUserCommand { - Task UpdateUserAsync(OrganizationUser organizationUser, Guid? savingUserId, + Task UpdateUserAsync(OrganizationUser organizationUser, OrganizationUserType existingUserType, Guid? savingUserId, List? collectionAccess, IEnumerable? groupAccess); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs index bad7b14b87..8d1e693e8b 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs @@ -55,11 +55,13 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand /// Update an organization user. /// /// The modified organization user to save. + /// The current type (member role) of the user. /// The userId of the currently logged in user who is making the change. /// The user's updated collection access. If set to null, this removes all collection access. /// The user's updated group access. If set to null, groups are not updated. /// - public async Task UpdateUserAsync(OrganizationUser organizationUser, Guid? savingUserId, + public async Task UpdateUserAsync(OrganizationUser organizationUser, OrganizationUserType existingUserType, + Guid? savingUserId, List? collectionAccess, IEnumerable? groupAccess) { // Avoid multiple enumeration @@ -83,15 +85,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand throw new NotFoundException(); } - if (organizationUser.UserId.HasValue && organization.PlanType == PlanType.Free && organizationUser.Type is OrganizationUserType.Admin or OrganizationUserType.Owner) - { - // Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this. - var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(organizationUser.UserId.Value); - if (adminCount > 0) - { - throw new BadRequestException("User can only be an admin of one free organization."); - } - } + await EnsureUserCannotBeAdminOrOwnerForMultipleFreeOrganizationAsync(organizationUser, existingUserType, organization); if (collectionAccessList.Count != 0) { @@ -151,6 +145,40 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Updated); } + private async Task EnsureUserCannotBeAdminOrOwnerForMultipleFreeOrganizationAsync(OrganizationUser updatedOrgUser, OrganizationUserType existingUserType, Entities.Organization organization) + { + + if (organization.PlanType != PlanType.Free) + { + return; + } + if (!updatedOrgUser.UserId.HasValue) + { + return; + } + if (updatedOrgUser.Type is not (OrganizationUserType.Admin or OrganizationUserType.Owner)) + { + return; + } + + // Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this. + var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(updatedOrgUser.UserId!.Value); + + var isCurrentAdminOrOwner = existingUserType is OrganizationUserType.Admin or OrganizationUserType.Owner; + + if (isCurrentAdminOrOwner && adminCount <= 1) + { + return; + } + + if (!isCurrentAdminOrOwner && adminCount == 0) + { + return; + } + + throw new BadRequestException("User can only be an admin of one free organization."); + } + private async Task ValidateCollectionAccessAsync(OrganizationUser originalUser, ICollection collectionAccess) { diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationUserControllerPutTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationUserControllerPutTests.cs index 542a76090c..e5fb3fe541 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationUserControllerPutTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationUserControllerPutTests.cs @@ -30,6 +30,7 @@ public class OrganizationUserControllerPutTests OrganizationUser organizationUser, OrganizationAbility organizationAbility, SutProvider sutProvider, Guid savingUserId) { + // Arrange Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess: []); // Authorize all changes for basic happy path test @@ -41,15 +42,18 @@ public class OrganizationUserControllerPutTests // Save these for later - organizationUser object will be mutated var orgUserId = organizationUser.Id; var orgUserEmail = organizationUser.Email; + var existingUserType = organizationUser.Type; + // Act await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model); + // Assert await sutProvider.GetDependency().Received(1).UpdateUserAsync(Arg.Is(ou => ou.Type == model.Type && ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && ou.AccessSecretsManager == model.AccessSecretsManager && ou.Id == orgUserId && - ou.Email == orgUserEmail), + ou.Email == orgUserEmail), existingUserType, savingUserId, Arg.Is>(cas => cas.All(c => model.Collections.Any(m => m.Id == c.Id))), @@ -77,6 +81,7 @@ public class OrganizationUserControllerPutTests OrganizationUser organizationUser, OrganizationAbility organizationAbility, SutProvider sutProvider, Guid savingUserId) { + // Arrange // Updating self organizationUser.UserId = savingUserId; organizationAbility.AllowAdminAccessToAllCollectionItems = false; @@ -88,15 +93,18 @@ public class OrganizationUserControllerPutTests var orgUserId = organizationUser.Id; var orgUserEmail = organizationUser.Email; + var existingUserType = organizationUser.Type; + // Act await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model); + // Assert await sutProvider.GetDependency().Received(1).UpdateUserAsync(Arg.Is(ou => - ou.Type == model.Type && - ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && - ou.AccessSecretsManager == model.AccessSecretsManager && - ou.Id == orgUserId && - ou.Email == orgUserEmail), + ou.Type == model.Type && + ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && + ou.AccessSecretsManager == model.AccessSecretsManager && + ou.Id == orgUserId && + ou.Email == orgUserEmail), existingUserType, savingUserId, Arg.Is>(cas => cas.All(c => model.Collections.Any(m => m.Id == c.Id))), @@ -110,6 +118,7 @@ public class OrganizationUserControllerPutTests OrganizationUser organizationUser, OrganizationAbility organizationAbility, SutProvider sutProvider, Guid savingUserId) { + // Arrange // Updating self organizationUser.UserId = savingUserId; organizationAbility.AllowAdminAccessToAllCollectionItems = true; @@ -121,15 +130,18 @@ public class OrganizationUserControllerPutTests var orgUserId = organizationUser.Id; var orgUserEmail = organizationUser.Email; + var existingUserType = organizationUser.Type; + // Act await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model); + // Assert await sutProvider.GetDependency().Received(1).UpdateUserAsync(Arg.Is(ou => - ou.Type == model.Type && - ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && - ou.AccessSecretsManager == model.AccessSecretsManager && - ou.Id == orgUserId && - ou.Email == orgUserEmail), + ou.Type == model.Type && + ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && + ou.AccessSecretsManager == model.AccessSecretsManager && + ou.Id == orgUserId && + ou.Email == orgUserEmail), existingUserType, savingUserId, Arg.Is>(cas => cas.All(c => model.Collections.Any(m => m.Id == c.Id))), @@ -142,6 +154,7 @@ public class OrganizationUserControllerPutTests OrganizationUser organizationUser, OrganizationAbility organizationAbility, SutProvider sutProvider, Guid savingUserId) { + // Arrange var editedCollectionId = CoreHelpers.GenerateComb(); var readonlyCollectionId1 = CoreHelpers.GenerateComb(); var readonlyCollectionId2 = CoreHelpers.GenerateComb(); @@ -194,16 +207,19 @@ public class OrganizationUserControllerPutTests .AuthorizeAsync(Arg.Any(), Arg.Is(c => c.Id == readonlyCollectionId1 || c.Id == readonlyCollectionId2), Arg.Is>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess))) .Returns(AuthorizationResult.Failed()); + var existingUserType = organizationUser.Type; + // Act await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model); + // Assert // Expect all collection access (modified and unmodified) to be saved await sutProvider.GetDependency().Received(1).UpdateUserAsync(Arg.Is(ou => - ou.Type == model.Type && - ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && - ou.AccessSecretsManager == model.AccessSecretsManager && - ou.Id == orgUserId && - ou.Email == orgUserEmail), + ou.Type == model.Type && + ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && + ou.AccessSecretsManager == model.AccessSecretsManager && + ou.Id == orgUserId && + ou.Email == orgUserEmail), existingUserType, savingUserId, Arg.Is>(cas => cas.Select(c => c.Id).SequenceEqual(currentCollectionAccess.Select(c => c.Id)) && diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs index cd03f9583b..5c07ce0347 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs @@ -27,8 +27,10 @@ public class UpdateOrganizationUserCommandTests List collections, List groups, SutProvider sutProvider) { user.Id = default(Guid); + var existingUserType = OrganizationUserType.User; + var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.UpdateUserAsync(user, savingUserId, collections, groups)); + () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, collections, groups)); Assert.Contains("invite the user first", exception.Message.ToLowerInvariant()); } @@ -37,9 +39,10 @@ public class UpdateOrganizationUserCommandTests Guid? savingUserId, SutProvider sutProvider) { sutProvider.GetDependency().GetByIdAsync(user.Id).Returns(originalUser); + var existingUserType = OrganizationUserType.User; await Assert.ThrowsAsync( - () => sutProvider.Sut.UpdateUserAsync(user, savingUserId, null, null)); + () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, null, null)); } [Theory, BitAutoData] @@ -55,8 +58,10 @@ public class UpdateOrganizationUserCommandTests .Returns(callInfo => callInfo.Arg>() .Select(guid => new Collection { Id = guid, OrganizationId = CoreHelpers.GenerateComb() }).ToList()); + var existingUserType = OrganizationUserType.User; + await Assert.ThrowsAsync( - () => sutProvider.Sut.UpdateUserAsync(user, savingUserId, collectionAccess, null)); + () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, collectionAccess, null)); } [Theory, BitAutoData] @@ -76,9 +81,9 @@ public class UpdateOrganizationUserCommandTests result.RemoveAt(0); return result; }); - + var existingUserType = OrganizationUserType.User; await Assert.ThrowsAsync( - () => sutProvider.Sut.UpdateUserAsync(user, savingUserId, collectionAccess, null)); + () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, collectionAccess, null)); } [Theory, BitAutoData] @@ -94,8 +99,10 @@ public class UpdateOrganizationUserCommandTests .Returns(callInfo => callInfo.Arg>() .Select(guid => new Group { Id = guid, OrganizationId = CoreHelpers.GenerateComb() }).ToList()); + var existingUserType = OrganizationUserType.User; + await Assert.ThrowsAsync( - () => sutProvider.Sut.UpdateUserAsync(user, savingUserId, null, groupAccess)); + () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, null, groupAccess)); } [Theory, BitAutoData] @@ -115,9 +122,9 @@ public class UpdateOrganizationUserCommandTests result.RemoveAt(0); return result; }); - + var existingUserType = OrganizationUserType.User; await Assert.ThrowsAsync( - () => sutProvider.Sut.UpdateUserAsync(user, savingUserId, null, groupAccess)); + () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, null, groupAccess)); } [Theory, BitAutoData] @@ -165,7 +172,9 @@ public class UpdateOrganizationUserCommandTests .GetCountByFreeOrganizationAdminUserAsync(newUserData.Id) .Returns(0); - await sutProvider.Sut.UpdateUserAsync(newUserData, savingUser.UserId, collections, groups); + var existingUserType = OrganizationUserType.User; + + await sutProvider.Sut.UpdateUserAsync(newUserData, existingUserType, savingUser.UserId, collections, groups); var organizationService = sutProvider.GetDependency(); await organizationService.Received(1).ValidateOrganizationUserUpdatePermissions( @@ -184,7 +193,7 @@ public class UpdateOrganizationUserCommandTests [Theory] [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Owner)] - public async Task UpdateUserAsync_WhenUpdatingUserToAdminOrOwner_WithUserAlreadyAdminOfAnotherFreeOrganization_Throws( + public async Task UpdateUserAsync_WhenUpdatingUserToAdminOrOwner_AndExistingUserTypeIsNotAdminOrOwner_WithUserAlreadyAdminOfAnotherFreeOrganization_Throws( OrganizationUserType userType, OrganizationUser oldUserData, OrganizationUser newUserData, @@ -199,10 +208,39 @@ public class UpdateOrganizationUserCommandTests sutProvider.GetDependency() .GetCountByFreeOrganizationAdminUserAsync(newUserData.UserId!.Value) .Returns(1); + var existingUserType = OrganizationUserType.User; // Assert var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.UpdateUserAsync(newUserData, null, null, null)); + () => sutProvider.Sut.UpdateUserAsync(newUserData, existingUserType, null, null, null)); + Assert.Contains("User can only be an admin of one free organization.", exception.Message); + } + + [Theory] + [BitAutoData(OrganizationUserType.Admin, OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Admin, OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Owner, OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner, OrganizationUserType.Owner)] + public async Task UpdateUserAsync_WhenUpdatingUserToAdminOrOwner_AndExistingUserTypeIsAdminOrOwner_WithUserAlreadyAdminOfAnotherFreeOrganization_Throws( + OrganizationUserType newUserType, + OrganizationUserType existingUserType, + OrganizationUser oldUserData, + OrganizationUser newUserData, + Organization organization, + SutProvider sutProvider) + { + organization.PlanType = PlanType.Free; + newUserData.Type = newUserType; + + Setup(sutProvider, organization, newUserData, oldUserData); + + sutProvider.GetDependency() + .GetCountByFreeOrganizationAdminUserAsync(newUserData.UserId!.Value) + .Returns(2); + + // Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpdateUserAsync(newUserData, existingUserType, null, null, null)); Assert.Contains("User can only be an admin of one free organization.", exception.Message); } From 66d1c70dc6bef05a272634e158030549231264b4 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 16 Jun 2025 13:09:36 +0000 Subject: [PATCH 02/47] Bumped version to 2025.6.2 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index a798c7f335..6a1a305e84 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.6.1 + 2025.6.2 Bit.$(MSBuildProjectName) enable From b8244908ecb17799a84f315372a4f898162f77f2 Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Mon, 16 Jun 2025 17:32:36 -0400 Subject: [PATCH 03/47] [PM-20112] Member access stored proc and splitting the query (#5943) --- src/Api/Dirt/Controllers/ReportsController.cs | 47 +- .../MemberAccessDetailReportResponseModel.cs | 39 + .../MemberCipherDetailsResponseModel.cs | 12 +- .../Models/Data/MemberAccessReportDetail.cs | 19 + .../Data/OrganizationMemberBaseDetail.cs | 19 + .../Models/Data/RiskInsightsReportDetail.cs | 10 + .../MemberAccessCipherDetailsQuery.cs | 206 -- .../ReportFeatures/MemberAccessReportQuery.cs | 65 + ...lsQuery.cs => IMemberAccessReportQuery.cs} | 4 +- .../Interfaces/IRiskInsightsReportQuery.cs | 9 + .../ReportingServiceCollectionExtensions.cs | 3 +- ...equest.cs => MemberAccessReportRequest.cs} | 2 +- .../Requests/RiskInsightsReportRequest.cs | 6 + .../ReportFeatures/RiskInsightsReportQuery.cs | 39 + ...IOrganizationMemberBaseDetailRepository.cs | 8 + .../DapperServiceCollectionExtensions.cs | 1 + .../OrganizationMemberBaseDetailRepository.cs | 39 + .../OrganizationMemberBaseDetailRepository.cs | 32 + ...ityFrameworkServiceCollectionExtensions.cs | 2 + .../Repositories/DatabaseContext.cs | 5 + ..._GetMemberAccessDetailByOrganizationId.sql | 92 + ...00_AddMemberAccessReportStoreProcedure.sql | 70 + ..._AlterMemberAccessReportStoreProcedure.sql | 94 + ...AccessReportStoreProcedure.sql.Designer.cs | 3166 ++++++++++++++++ ...AddMemberAccessReportStoreProcedure.sql.cs | 50 + .../DatabaseContextModelSnapshot.cs | 125 +- ...AccessReportStoreProcedure.sql.Designer.cs | 3172 +++++++++++++++++ ...AddMemberAccessReportStoreProcedure.sql.cs | 43 + .../DatabaseContextModelSnapshot.cs | 125 +- ...AccessReportStoreProcedure.sql.Designer.cs | 3155 ++++++++++++++++ ...AddMemberAccessReportStoreProcedure.sql.cs | 43 + .../DatabaseContextModelSnapshot.cs | 125 +- 32 files changed, 10480 insertions(+), 347 deletions(-) create mode 100644 src/Api/Dirt/Models/Response/MemberAccessDetailReportResponseModel.cs create mode 100644 src/Core/Dirt/Reports/Models/Data/MemberAccessReportDetail.cs create mode 100644 src/Core/Dirt/Reports/Models/Data/OrganizationMemberBaseDetail.cs create mode 100644 src/Core/Dirt/Reports/Models/Data/RiskInsightsReportDetail.cs delete mode 100644 src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs rename src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/{IMemberAccessCipherDetailsQuery.cs => IMemberAccessReportQuery.cs} (52%) create mode 100644 src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IRiskInsightsReportQuery.cs rename src/Core/Dirt/Reports/ReportFeatures/Requests/{MemberAccessCipherDetailsRequest.cs => MemberAccessReportRequest.cs} (70%) create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Requests/RiskInsightsReportRequest.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/RiskInsightsReportQuery.cs create mode 100644 src/Core/Dirt/Reports/Repositories/IOrganizationMemberBaseDetailRepository.cs create mode 100644 src/Infrastructure.Dapper/Dirt/OrganizationMemberBaseDetailRepository.cs create mode 100644 src/Infrastructure.EntityFramework/Dirt/OrganizationMemberBaseDetailRepository.cs create mode 100644 src/Sql/Dirt/Stored Procedure/MemberAccessDetail_GetMemberAccessDetailByOrganizationId.sql create mode 100644 util/Migrator/DbScripts/2025-06-09_00_AddMemberAccessReportStoreProcedure.sql create mode 100644 util/Migrator/DbScripts/2025-06-12_00_AlterMemberAccessReportStoreProcedure.sql create mode 100644 util/MySqlMigrations/Migrations/20250609182150_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250609182150_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.cs create mode 100644 util/PostgresMigrations/Migrations/20250609182157_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250609182157_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.cs create mode 100644 util/SqliteMigrations/Migrations/20250609182153_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250609182153_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.cs diff --git a/src/Api/Dirt/Controllers/ReportsController.cs b/src/Api/Dirt/Controllers/ReportsController.cs index 2f7a5a4328..8281bdaa98 100644 --- a/src/Api/Dirt/Controllers/ReportsController.cs +++ b/src/Api/Dirt/Controllers/ReportsController.cs @@ -1,5 +1,6 @@ using Bit.Api.Dirt.Models; using Bit.Api.Dirt.Models.Response; +using Bit.Api.Tools.Models.Response; using Bit.Core.Context; using Bit.Core.Dirt.Reports.Entities; using Bit.Core.Dirt.Reports.Models.Data; @@ -17,21 +18,24 @@ namespace Bit.Api.Dirt.Controllers; public class ReportsController : Controller { private readonly ICurrentContext _currentContext; - private readonly IMemberAccessCipherDetailsQuery _memberAccessCipherDetailsQuery; + private readonly IMemberAccessReportQuery _memberAccessReportQuery; + private readonly IRiskInsightsReportQuery _riskInsightsReportQuery; private readonly IAddPasswordHealthReportApplicationCommand _addPwdHealthReportAppCommand; private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery; private readonly IDropPasswordHealthReportApplicationCommand _dropPwdHealthReportAppCommand; public ReportsController( ICurrentContext currentContext, - IMemberAccessCipherDetailsQuery memberAccessCipherDetailsQuery, + IMemberAccessReportQuery memberAccessReportQuery, + IRiskInsightsReportQuery riskInsightsReportQuery, IAddPasswordHealthReportApplicationCommand addPasswordHealthReportApplicationCommand, IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery, IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand ) { _currentContext = currentContext; - _memberAccessCipherDetailsQuery = memberAccessCipherDetailsQuery; + _memberAccessReportQuery = memberAccessReportQuery; + _riskInsightsReportQuery = riskInsightsReportQuery; _addPwdHealthReportAppCommand = addPasswordHealthReportApplicationCommand; _getPwdHealthReportAppQuery = getPasswordHealthReportApplicationQuery; _dropPwdHealthReportAppCommand = dropPwdHealthReportAppCommand; @@ -54,9 +58,9 @@ public class ReportsController : Controller throw new NotFoundException(); } - var memberCipherDetails = await GetMemberCipherDetails(new MemberAccessCipherDetailsRequest { OrganizationId = orgId }); + var riskDetails = await GetRiskInsightsReportDetails(new RiskInsightsReportRequest { OrganizationId = orgId }); - var responses = memberCipherDetails.Select(x => new MemberCipherDetailsResponseModel(x)); + var responses = riskDetails.Select(x => new MemberCipherDetailsResponseModel(x)); return responses; } @@ -69,16 +73,16 @@ public class ReportsController : Controller /// IEnumerable of MemberAccessReportResponseModel /// If Access reports permission is not assigned [HttpGet("member-access/{orgId}")] - public async Task> GetMemberAccessReport(Guid orgId) + public async Task> GetMemberAccessReport(Guid orgId) { if (!await _currentContext.AccessReports(orgId)) { throw new NotFoundException(); } - var memberCipherDetails = await GetMemberCipherDetails(new MemberAccessCipherDetailsRequest { OrganizationId = orgId }); + var accessDetails = await GetMemberAccessDetails(new MemberAccessReportRequest { OrganizationId = orgId }); - var responses = memberCipherDetails.Select(x => new MemberAccessReportResponseModel(x)); + var responses = accessDetails.Select(x => new MemberAccessDetailReportResponseModel(x)); return responses; } @@ -87,13 +91,28 @@ public class ReportsController : Controller /// Contains the organization member info, the cipher ids associated with the member, /// and details on their collections, groups, and permissions /// - /// Request to the MemberAccessCipherDetailsQuery - /// IEnumerable of MemberAccessCipherDetails - private async Task> GetMemberCipherDetails(MemberAccessCipherDetailsRequest request) + /// 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 memberCipherDetails = - await _memberAccessCipherDetailsQuery.GetMemberAccessCipherDetails(request); - return memberCipherDetails; + 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 + /// + /// Request parameters + /// A list of risk insights data associating the user to cipher ids + private async Task> GetRiskInsightsReportDetails( + RiskInsightsReportRequest request) + { + var riskDetails = await _riskInsightsReportQuery.GetRiskInsightsReportDetails(request); + return riskDetails; } /// diff --git a/src/Api/Dirt/Models/Response/MemberAccessDetailReportResponseModel.cs b/src/Api/Dirt/Models/Response/MemberAccessDetailReportResponseModel.cs new file mode 100644 index 0000000000..2d5a7b1556 --- /dev/null +++ b/src/Api/Dirt/Models/Response/MemberAccessDetailReportResponseModel.cs @@ -0,0 +1,39 @@ +using Bit.Core.Dirt.Reports.Models.Data; + +namespace Bit.Api.Tools.Models.Response; + +public class MemberAccessDetailReportResponseModel +{ + public Guid? UserGuid { get; set; } + public string UserName { get; set; } + public string Email { get; set; } + public bool TwoFactorEnabled { get; set; } + public bool AccountRecoveryEnabled { get; set; } + public bool UsesKeyConnector { get; set; } + public Guid? CollectionId { get; set; } + public Guid? GroupId { get; set; } + public string GroupName { get; set; } + public string CollectionName { get; set; } + public bool? ReadOnly { get; set; } + public bool? HidePasswords { get; set; } + public bool? Manage { get; set; } + public IEnumerable CipherIds { get; set; } + + public MemberAccessDetailReportResponseModel(MemberAccessReportDetail reportDetail) + { + UserGuid = reportDetail.UserGuid; + UserName = reportDetail.UserName; + Email = reportDetail.Email; + TwoFactorEnabled = reportDetail.TwoFactorEnabled; + AccountRecoveryEnabled = reportDetail.AccountRecoveryEnabled; + UsesKeyConnector = reportDetail.UsesKeyConnector; + CollectionId = reportDetail.CollectionId; + GroupId = reportDetail.GroupId; + GroupName = reportDetail.GroupName; + CollectionName = reportDetail.CollectionName; + ReadOnly = reportDetail.ReadOnly; + HidePasswords = reportDetail.HidePasswords; + Manage = reportDetail.Manage; + CipherIds = reportDetail.CipherIds; + } +} diff --git a/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs b/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs index 30065ad05a..e5c6235de3 100644 --- a/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs +++ b/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs @@ -15,12 +15,12 @@ public class MemberCipherDetailsResponseModel /// public IEnumerable CipherIds { get; set; } - public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails) + public MemberCipherDetailsResponseModel(RiskInsightsReportDetail reportDetail) { - this.UserGuid = memberAccessCipherDetails.UserGuid; - this.UserName = memberAccessCipherDetails.UserName; - this.Email = memberAccessCipherDetails.Email; - this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector; - this.CipherIds = memberAccessCipherDetails.CipherIds; + this.UserGuid = reportDetail.UserGuid; + this.UserName = reportDetail.UserName; + this.Email = reportDetail.Email; + this.UsesKeyConnector = reportDetail.UsesKeyConnector; + this.CipherIds = reportDetail.CipherIds; } } diff --git a/src/Core/Dirt/Reports/Models/Data/MemberAccessReportDetail.cs b/src/Core/Dirt/Reports/Models/Data/MemberAccessReportDetail.cs new file mode 100644 index 0000000000..a99b6e2088 --- /dev/null +++ b/src/Core/Dirt/Reports/Models/Data/MemberAccessReportDetail.cs @@ -0,0 +1,19 @@ +namespace Bit.Core.Dirt.Reports.Models.Data; + +public class MemberAccessReportDetail +{ + public Guid? UserGuid { get; set; } + public string UserName { get; set; } + public string Email { get; set; } + public bool TwoFactorEnabled { get; set; } + public bool AccountRecoveryEnabled { get; set; } + public bool UsesKeyConnector { get; set; } + public Guid? CollectionId { get; set; } + public Guid? GroupId { get; set; } + public string GroupName { get; set; } + public string CollectionName { get; set; } + public bool? ReadOnly { get; set; } + public bool? HidePasswords { get; set; } + public bool? Manage { get; set; } + public IEnumerable CipherIds { get; set; } +} diff --git a/src/Core/Dirt/Reports/Models/Data/OrganizationMemberBaseDetail.cs b/src/Core/Dirt/Reports/Models/Data/OrganizationMemberBaseDetail.cs new file mode 100644 index 0000000000..a1f0bd81fd --- /dev/null +++ b/src/Core/Dirt/Reports/Models/Data/OrganizationMemberBaseDetail.cs @@ -0,0 +1,19 @@ +namespace Bit.Core.Dirt.Reports.Models.Data; + +public class OrganizationMemberBaseDetail +{ + public Guid? UserGuid { get; set; } + public string UserName { get; set; } + public string Email { get; set; } + public string TwoFactorProviders { get; set; } + public bool UsesKeyConnector { get; set; } + public string ResetPasswordKey { get; set; } + public Guid? CollectionId { get; set; } + public Guid? GroupId { get; set; } + public string GroupName { get; set; } + public string CollectionName { get; set; } + public bool? ReadOnly { get; set; } + public bool? HidePasswords { get; set; } + public bool? Manage { get; set; } + public Guid CipherId { get; set; } +} diff --git a/src/Core/Dirt/Reports/Models/Data/RiskInsightsReportDetail.cs b/src/Core/Dirt/Reports/Models/Data/RiskInsightsReportDetail.cs new file mode 100644 index 0000000000..1ea805edf1 --- /dev/null +++ b/src/Core/Dirt/Reports/Models/Data/RiskInsightsReportDetail.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Dirt.Reports.Models.Data; + +public class RiskInsightsReportDetail +{ + public Guid? UserGuid { get; set; } + public string UserName { get; set; } + public string Email { get; set; } + public bool UsesKeyConnector { get; set; } + public IEnumerable CipherIds { get; set; } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs deleted file mode 100644 index 4a8039e6bc..0000000000 --- a/src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System.Collections.Concurrent; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Repositories; -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; -using Bit.Core.Entities; -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; -using Bit.Core.Vault.Models.Data; -using Bit.Core.Vault.Queries; -using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; -using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; - -namespace Bit.Core.Dirt.Reports.ReportFeatures; - -public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery -{ - private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery; - private readonly IGroupRepository _groupRepository; - private readonly ICollectionRepository _collectionRepository; - private readonly IOrganizationCiphersQuery _organizationCiphersQuery; - private readonly IApplicationCacheService _applicationCacheService; - private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; - - public MemberAccessCipherDetailsQuery( - IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery, - IGroupRepository groupRepository, - ICollectionRepository collectionRepository, - IOrganizationCiphersQuery organizationCiphersQuery, - IApplicationCacheService applicationCacheService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery - ) - { - _organizationUserUserDetailsQuery = organizationUserUserDetailsQuery; - _groupRepository = groupRepository; - _collectionRepository = collectionRepository; - _organizationCiphersQuery = organizationCiphersQuery; - _applicationCacheService = applicationCacheService; - _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; - } - - public async Task> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request) - { - var orgUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails( - new OrganizationUserUserDetailsQueryRequest - { - OrganizationId = request.OrganizationId, - IncludeCollections = true, - IncludeGroups = true - }); - - var orgGroups = await _groupRepository.GetManyByOrganizationIdAsync(request.OrganizationId); - var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(request.OrganizationId); - var orgCollectionsWithAccess = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(request.OrganizationId); - var orgItems = await _organizationCiphersQuery.GetAllOrganizationCiphers(request.OrganizationId); - var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers); - - var memberAccessCipherDetails = GenerateAccessDataParallel( - orgGroups, - orgCollectionsWithAccess, - orgItems, - organizationUsersTwoFactorEnabled, - orgAbility); - - return memberAccessCipherDetails; - } - - /// - /// Generates a report for all members of an organization. Containing summary information - /// such as item, collection, and group counts. Including the cipherIds a member is assigned. - /// Child collection includes detailed information on the user and group collections along - /// with their permissions. - /// - /// Organization groups collection - /// Collections for the organization and the groups/users and permissions - /// Cipher items for the organization with the collections associated with them - /// Organization users and two factor status - /// Organization ability for account recovery status - /// List of the MemberAccessCipherDetailsModel; - private IEnumerable GenerateAccessDataParallel( - ICollection orgGroups, - ICollection> orgCollectionsWithAccess, - IEnumerable orgItems, - IEnumerable<(OrganizationUserUserDetails user, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled, - OrganizationAbility orgAbility) - { - var orgUsers = organizationUsersTwoFactorEnabled.Select(x => x.user).ToList(); - var groupNameDictionary = orgGroups.ToDictionary(x => x.Id, x => x.Name); - var collectionItems = orgItems - .SelectMany(x => x.CollectionIds, - (cipher, collectionId) => new { Cipher = cipher, CollectionId = collectionId }) - .GroupBy(y => y.CollectionId, - (key, ciphers) => new { CollectionId = key, Ciphers = ciphers }); - var itemLookup = collectionItems.ToDictionary(x => x.CollectionId.ToString(), x => x.Ciphers.Select(c => c.Cipher.Id.ToString()).ToList()); - - var memberAccessCipherDetails = new ConcurrentBag(); - - Parallel.ForEach(orgUsers, user => - { - var groupAccessDetails = new List(); - var userCollectionAccessDetails = new List(); - - foreach (var tCollect in orgCollectionsWithAccess) - { - if (itemLookup.TryGetValue(tCollect.Item1.Id.ToString(), out var items)) - { - var itemCounts = items.Count; - - if (tCollect.Item2.Groups.Any()) - { - var groupDetails = tCollect.Item2.Groups - .Where(tCollectGroups => user.Groups.Contains(tCollectGroups.Id)) - .Select(x => new MemberAccessDetails - { - CollectionId = tCollect.Item1.Id, - CollectionName = tCollect.Item1.Name, - GroupId = x.Id, - GroupName = groupNameDictionary[x.Id], - ReadOnly = x.ReadOnly, - HidePasswords = x.HidePasswords, - Manage = x.Manage, - ItemCount = itemCounts, - CollectionCipherIds = items - }); - - groupAccessDetails.AddRange(groupDetails); - } - - if (tCollect.Item2.Users.Any()) - { - var userCollectionDetails = tCollect.Item2.Users - .Where(tCollectUser => tCollectUser.Id == user.Id) - .Select(x => new MemberAccessDetails - { - CollectionId = tCollect.Item1.Id, - CollectionName = tCollect.Item1.Name, - ReadOnly = x.ReadOnly, - HidePasswords = x.HidePasswords, - Manage = x.Manage, - ItemCount = itemCounts, - CollectionCipherIds = items - }); - - userCollectionAccessDetails.AddRange(userCollectionDetails); - } - } - } - - var report = new MemberAccessCipherDetails - { - UserName = user.Name, - Email = user.Email, - TwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == user.Id).twoFactorIsEnabled, - AccountRecoveryEnabled = !string.IsNullOrEmpty(user.ResetPasswordKey) && orgAbility.UseResetPassword, - UserGuid = user.Id, - UsesKeyConnector = user.UsesKeyConnector - }; - - var userAccessDetails = new List(); - if (user.Groups.Any()) - { - var userGroups = groupAccessDetails.Where(x => user.Groups.Contains(x.GroupId.GetValueOrDefault())); - userAccessDetails.AddRange(userGroups); - } - - var groupsWithoutCollections = user.Groups.Where(x => !userAccessDetails.Any(y => x == y.GroupId)); - if (groupsWithoutCollections.Any()) - { - var emptyGroups = groupsWithoutCollections.Select(x => new MemberAccessDetails - { - GroupId = x, - GroupName = groupNameDictionary[x], - ItemCount = 0 - }); - userAccessDetails.AddRange(emptyGroups); - } - - if (user.Collections.Any()) - { - var userCollections = userCollectionAccessDetails.Where(x => user.Collections.Any(y => x.CollectionId == y.Id)); - userAccessDetails.AddRange(userCollections); - } - report.AccessDetails = userAccessDetails; - - var userCiphers = report.AccessDetails - .Where(x => x.ItemCount > 0) - .SelectMany(y => y.CollectionCipherIds) - .Distinct(); - report.CipherIds = userCiphers; - report.TotalItemCount = userCiphers.Count(); - - var distinctItems = report.AccessDetails.Where(x => x.CollectionId.HasValue).Select(x => x.CollectionId).Distinct(); - report.CollectionsCount = distinctItems.Count(); - report.GroupsCount = report.AccessDetails.Select(x => x.GroupId).Where(y => y.HasValue).Distinct().Count(); - - memberAccessCipherDetails.Add(report); - }); - - return memberAccessCipherDetails; - } -} diff --git a/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs new file mode 100644 index 0000000000..7ab8acb8dc --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs @@ -0,0 +1,65 @@ +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; +using Bit.Core.Dirt.Reports.Repositories; +using Bit.Core.Services; + +namespace Bit.Core.Dirt.Reports.ReportFeatures; + +public class MemberAccessReportQuery( + IOrganizationMemberBaseDetailRepository organizationMemberBaseDetailRepository, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + IApplicationCacheService applicationCacheService) + : IMemberAccessReportQuery +{ + public async Task> GetMemberAccessReportsAsync( + MemberAccessReportRequest request) + { + var baseDetails = + await organizationMemberBaseDetailRepository.GetOrganizationMemberBaseDetailsByOrganizationId( + request.OrganizationId); + + var orgUsers = baseDetails.Select(x => x.UserGuid.GetValueOrDefault()).Distinct(); + var orgUsersTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers); + + var orgAbility = await applicationCacheService.GetOrganizationAbilityAsync(request.OrganizationId); + + var accessDetails = baseDetails + .GroupBy(b => new + { + b.UserGuid, + b.UserName, + b.Email, + b.TwoFactorProviders, + b.ResetPasswordKey, + b.UsesKeyConnector, + b.GroupId, + b.GroupName, + b.CollectionId, + b.CollectionName, + b.ReadOnly, + b.HidePasswords, + b.Manage + }) + .Select(g => new MemberAccessReportDetail + { + UserGuid = g.Key.UserGuid, + UserName = g.Key.UserName, + Email = g.Key.Email, + TwoFactorEnabled = orgUsersTwoFactorEnabled.FirstOrDefault(x => x.userId == g.Key.UserGuid).twoFactorIsEnabled, + AccountRecoveryEnabled = !string.IsNullOrWhiteSpace(g.Key.ResetPasswordKey) && orgAbility.UseResetPassword, + UsesKeyConnector = g.Key.UsesKeyConnector, + GroupId = g.Key.GroupId, + GroupName = g.Key.GroupName, + CollectionId = g.Key.CollectionId, + CollectionName = g.Key.CollectionName, + ReadOnly = g.Key.ReadOnly, + HidePasswords = g.Key.HidePasswords, + Manage = g.Key.Manage, + CipherIds = g.Select(c => c.CipherId) + }); + + return accessDetails; + } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessReportQuery.cs similarity index 52% rename from src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs rename to src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessReportQuery.cs index 98ed780db3..44bb4f33c5 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessReportQuery.cs @@ -3,7 +3,7 @@ using Bit.Core.Dirt.Reports.ReportFeatures.Requests; namespace Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces; -public interface IMemberAccessCipherDetailsQuery +public interface IMemberAccessReportQuery { - Task> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request); + Task> GetMemberAccessReportsAsync(MemberAccessReportRequest request); } diff --git a/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IRiskInsightsReportQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IRiskInsightsReportQuery.cs new file mode 100644 index 0000000000..c6ba69dfff --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IRiskInsightsReportQuery.cs @@ -0,0 +1,9 @@ +using Bit.Core.Dirt.Reports.Models.Data; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; + +namespace Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces; + +public interface IRiskInsightsReportQuery +{ + Task> GetRiskInsightsReportDetails(RiskInsightsReportRequest request); +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs b/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs index d847c8051e..4339d0f2f4 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs @@ -8,7 +8,8 @@ public static class ReportingServiceCollectionExtensions { public static void AddReportingServices(this IServiceCollection services) { - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Core/Dirt/Reports/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/MemberAccessReportRequest.cs similarity index 70% rename from src/Core/Dirt/Reports/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs rename to src/Core/Dirt/Reports/ReportFeatures/Requests/MemberAccessReportRequest.cs index b40dfc6dec..5fe28810a6 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/MemberAccessReportRequest.cs @@ -1,6 +1,6 @@ namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; -public class MemberAccessCipherDetailsRequest +public class MemberAccessReportRequest { public Guid OrganizationId { get; set; } } diff --git a/src/Core/Dirt/Reports/ReportFeatures/Requests/RiskInsightsReportRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/RiskInsightsReportRequest.cs new file mode 100644 index 0000000000..1b843ea002 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/RiskInsightsReportRequest.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; + +public class RiskInsightsReportRequest +{ + public Guid OrganizationId { get; set; } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/RiskInsightsReportQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/RiskInsightsReportQuery.cs new file mode 100644 index 0000000000..e686698c51 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/RiskInsightsReportQuery.cs @@ -0,0 +1,39 @@ +using Bit.Core.Dirt.Reports.Models.Data; +using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; +using Bit.Core.Dirt.Reports.Repositories; + +namespace Bit.Core.Dirt.Reports.ReportFeatures; + +public class RiskInsightsReportQuery : IRiskInsightsReportQuery +{ + private readonly IOrganizationMemberBaseDetailRepository _organizationMemberBaseDetailRepository; + + public RiskInsightsReportQuery(IOrganizationMemberBaseDetailRepository repository) + { + _organizationMemberBaseDetailRepository = repository; + } + + public async Task> GetRiskInsightsReportDetails( + RiskInsightsReportRequest request) + { + var baseDetails = + await _organizationMemberBaseDetailRepository.GetOrganizationMemberBaseDetailsByOrganizationId( + request.OrganizationId); + + var insightsDetails = baseDetails + .GroupBy(b => new { b.UserGuid, b.UserName, b.Email, b.UsesKeyConnector }) + .Select(g => new RiskInsightsReportDetail + { + UserGuid = g.Key.UserGuid, + UserName = g.Key.UserName, + Email = g.Key.Email, + UsesKeyConnector = g.Key.UsesKeyConnector, + CipherIds = g + .Select(x => x.CipherId.ToString()) + .Distinct() + }); + + return insightsDetails; + } +} diff --git a/src/Core/Dirt/Reports/Repositories/IOrganizationMemberBaseDetailRepository.cs b/src/Core/Dirt/Reports/Repositories/IOrganizationMemberBaseDetailRepository.cs new file mode 100644 index 0000000000..e2a161aa9c --- /dev/null +++ b/src/Core/Dirt/Reports/Repositories/IOrganizationMemberBaseDetailRepository.cs @@ -0,0 +1,8 @@ +using Bit.Core.Dirt.Reports.Models.Data; + +namespace Bit.Core.Dirt.Reports.Repositories; + +public interface IOrganizationMemberBaseDetailRepository +{ + Task> GetOrganizationMemberBaseDetailsByOrganizationId(Guid organizationId); +} diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index a95c2bd4c6..e64eabd5bf 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -70,6 +70,7 @@ public static class DapperServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); if (selfHosted) { diff --git a/src/Infrastructure.Dapper/Dirt/OrganizationMemberBaseDetailRepository.cs b/src/Infrastructure.Dapper/Dirt/OrganizationMemberBaseDetailRepository.cs new file mode 100644 index 0000000000..458e72f996 --- /dev/null +++ b/src/Infrastructure.Dapper/Dirt/OrganizationMemberBaseDetailRepository.cs @@ -0,0 +1,39 @@ +using System.Data; +using Bit.Core.Dirt.Reports.Models.Data; +using Bit.Core.Dirt.Reports.Repositories; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; + +namespace Bit.Infrastructure.Dapper.Dirt; + +public class OrganizationMemberBaseDetailRepository : BaseRepository, IOrganizationMemberBaseDetailRepository +{ + public OrganizationMemberBaseDetailRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { + } + + public OrganizationMemberBaseDetailRepository(string connectionString, string readOnlyConnectionString) : base( + connectionString, readOnlyConnectionString) + { + } + + public async Task> GetOrganizationMemberBaseDetailsByOrganizationId( + Guid organizationId) + { + await using var connection = new SqlConnection(ConnectionString); + + + var result = await connection.QueryAsync( + "[dbo].[MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId]", + new + { + OrganizationId = organizationId + + }, commandType: CommandType.StoredProcedure); + + return result; + } +} diff --git a/src/Infrastructure.EntityFramework/Dirt/OrganizationMemberBaseDetailRepository.cs b/src/Infrastructure.EntityFramework/Dirt/OrganizationMemberBaseDetailRepository.cs new file mode 100644 index 0000000000..123379da90 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Dirt/OrganizationMemberBaseDetailRepository.cs @@ -0,0 +1,32 @@ +using AutoMapper; +using Bit.Core.Dirt.Reports.Models.Data; +using Bit.Core.Dirt.Reports.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Infrastructure.EntityFramework.Dirt; + +public class OrganizationMemberBaseDetailRepository : BaseEntityFrameworkRepository, IOrganizationMemberBaseDetailRepository +{ + public OrganizationMemberBaseDetailRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : base( + serviceScopeFactory, + mapper) + { + } + + public async Task> GetOrganizationMemberBaseDetailsByOrganizationId( + Guid organizationId) + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + + var result = await dbContext.Set() + .FromSqlRaw("EXEC [dbo].[MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId] @OrganizationId", + new SqlParameter("@OrganizationId", organizationId)) + .ToListAsync(); + + return result; + } +} diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index 321c4c90e5..616b2bc434 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -14,6 +14,7 @@ using Bit.Core.Vault.Repositories; using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; using Bit.Infrastructure.EntityFramework.Auth.Repositories; using Bit.Infrastructure.EntityFramework.Billing.Repositories; +using Bit.Infrastructure.EntityFramework.Dirt; using Bit.Infrastructure.EntityFramework.Dirt.Repositories; using Bit.Infrastructure.EntityFramework.KeyManagement.Repositories; using Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories; @@ -107,6 +108,7 @@ public static class EntityFrameworkServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); if (selfHosted) { diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs index 647b3e3ab1..e1e29cbf41 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs @@ -1,4 +1,5 @@ using Bit.Core; +using Bit.Core.Dirt.Reports.Models.Data; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; using Bit.Infrastructure.EntityFramework.Auth.Models; @@ -80,6 +81,7 @@ public class DatabaseContext : DbContext public DbSet NotificationStatuses { get; set; } public DbSet ClientOrganizationMigrationRecords { get; set; } public DbSet PasswordHealthReportApplications { get; set; } + public DbSet OrganizationMemberBaseDetails { get; set; } public DbSet SecurityTasks { get; set; } public DbSet OrganizationInstallations { get; set; } @@ -112,6 +114,7 @@ public class DatabaseContext : DbContext var eOrganizationConnection = builder.Entity(); var eOrganizationDomain = builder.Entity(); var aWebAuthnCredential = builder.Entity(); + var eOrganizationMemberBaseDetail = builder.Entity(); // Shadow property configurations go here @@ -134,6 +137,8 @@ public class DatabaseContext : DbContext eCollectionGroup.HasKey(cg => new { cg.CollectionId, cg.GroupId }); eGroupUser.HasKey(gu => new { gu.GroupId, gu.OrganizationUserId }); + eOrganizationMemberBaseDetail.HasNoKey(); + var dataProtector = this.GetService().CreateProtector( Constants.DatabaseFieldProtectorPurpose); var dataProtectionConverter = new DataProtectionConverter(dataProtector); diff --git a/src/Sql/Dirt/Stored Procedure/MemberAccessDetail_GetMemberAccessDetailByOrganizationId.sql b/src/Sql/Dirt/Stored Procedure/MemberAccessDetail_GetMemberAccessDetailByOrganizationId.sql new file mode 100644 index 0000000000..1aaf667f6a --- /dev/null +++ b/src/Sql/Dirt/Stored Procedure/MemberAccessDetail_GetMemberAccessDetailByOrganizationId.sql @@ -0,0 +1,92 @@ +CREATE PROCEDURE dbo.MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId + @OrganizationId UNIQUEIDENTIFIER +AS + SET NOCOUNT ON; + +IF @OrganizationId IS NULL + THROW 50000, 'OrganizationId cannot be null', 1; + + SELECT + OU.Id AS UserGuid, + U.Name AS UserName, + ISNULL(U.Email, OU.Email) as 'Email', + U.TwoFactorProviders, + U.UsesKeyConnector, + OU.ResetPasswordKey, + CC.CollectionId, + C.Name AS CollectionName, + NULL AS GroupId, + NULL AS GroupName, + CU.ReadOnly, + CU.HidePasswords, + CU.Manage, + Cipher.Id AS CipherId + FROM dbo.OrganizationUser OU + LEFT JOIN dbo.[User] U ON U.Id = OU.UserId + INNER JOIN dbo.Organization O ON O.Id = OU.OrganizationId + AND O.Id = @OrganizationId + AND O.Enabled = 1 + INNER JOIN dbo.CollectionUser CU ON CU.OrganizationUserId = OU.Id + INNER JOIN dbo.Collection C ON C.Id = CU.CollectionId and C.OrganizationId = @OrganizationId + INNER JOIN dbo.CollectionCipher CC ON CC.CollectionId = C.Id + INNER JOIN dbo.Cipher Cipher ON Cipher.Id = CC.CipherId AND Cipher.OrganizationId = @OrganizationId + WHERE OU.Status IN (0,1,2) -- Invited, Accepted and Confirmed Users + AND Cipher.DeletedDate IS NULL +UNION ALL + -- Group-based collection permissions + SELECT + OU.Id AS UserGuid, + U.Name AS UserName, + ISNULL(U.Email, OU.Email) as 'Email', + U.TwoFactorProviders, + U.UsesKeyConnector, + OU.ResetPasswordKey, + CC.CollectionId, + C.Name AS CollectionName, + G.Id AS GroupId, + G.Name AS GroupName, + CG.ReadOnly, + CG.HidePasswords, + CG.Manage, + Cipher.Id AS CipherId + FROM dbo.OrganizationUser OU + LEFT JOIN dbo.[User] U ON U.Id = OU.UserId + INNER JOIN dbo.Organization O ON O.Id = OU.OrganizationId + AND O.Id = @OrganizationId + AND O.Enabled = 1 + INNER JOIN dbo.GroupUser GU ON GU.OrganizationUserId = OU.Id + INNER JOIN dbo.[Group] G ON G.Id = GU.GroupId + INNER JOIN dbo.CollectionGroup CG ON CG.GroupId = G.Id + INNER JOIN dbo.Collection C ON C.Id = CG.CollectionId AND C.OrganizationId = @OrganizationId + INNER JOIN dbo.CollectionCipher CC ON CC.CollectionId = C.Id + INNER JOIN dbo.Cipher Cipher ON Cipher.Id = CC.CipherId and Cipher.OrganizationId = @OrganizationId + WHERE OU.Status IN (0,1,2) -- Invited, Accepted and Confirmed Users + AND Cipher.DeletedDate IS NULL +UNION ALL + -- Users without collection access (invited users) + -- typically invited users who have not yet accepted the invitation + -- and not yet assigned to any collection + SELECT + OU.Id AS UserGuid, + U.Name AS UserName, + ISNULL(U.Email, OU.Email) as 'Email', + U.TwoFactorProviders, + U.UsesKeyConnector, + OU.ResetPasswordKey, + null as CollectionId, + null AS CollectionName, + NULL AS GroupId, + NULL AS GroupName, + null as [ReadOnly], + null as HidePasswords, + null as Manage, + null AS CipherId + FROM dbo.OrganizationUser OU + LEFT JOIN dbo.[User] U ON U.Id = OU.UserId + INNER JOIN dbo.Organization O ON O.Id = OU.OrganizationId AND O.Id = @OrganizationId AND O.Enabled = 1 + WHERE OU.Status IN (0,1,2) -- Invited, Accepted and Confirmed Users + AND OU.Id not in ( + select OU1.Id from dbo.OrganizationUser OU1 + inner join dbo.CollectionUser CU1 on CU1.OrganizationUserId = OU1.Id + WHERE OU1.OrganizationId = @organizationId + ) diff --git a/util/Migrator/DbScripts/2025-06-09_00_AddMemberAccessReportStoreProcedure.sql b/util/Migrator/DbScripts/2025-06-09_00_AddMemberAccessReportStoreProcedure.sql new file mode 100644 index 0000000000..afb682e97a --- /dev/null +++ b/util/Migrator/DbScripts/2025-06-09_00_AddMemberAccessReportStoreProcedure.sql @@ -0,0 +1,70 @@ +CREATE OR ALTER PROC dbo.MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId + @OrganizationId UNIQUEIDENTIFIER +AS + SET NOCOUNT ON; + + IF @OrganizationId IS NULL + THROW 50000, 'OrganizationId cannot be null', 1; + +SELECT + U.Id AS UserGuid, + U.Name AS UserName, + U.Email, + U.TwoFactorProviders, + U.UsesKeyConnector, + OU.ResetPasswordKey, + CC.CollectionId, + C.Name AS CollectionName, + NULL AS GroupId, + NULL AS GroupName, + CU.ReadOnly, + CU.HidePasswords, + CU.Manage, + Cipher.Id AS CipherId +FROM dbo.OrganizationUser OU + INNER JOIN dbo.[User] U ON U.Id = OU.UserId + INNER JOIN dbo.Organization O ON O.Id = OU.OrganizationId + AND O.Id = @OrganizationId + AND O.Enabled = 1 + INNER JOIN dbo.CollectionUser CU ON CU.OrganizationUserId = OU.Id + INNER JOIN dbo.Collection C ON C.Id = CU.CollectionId + INNER JOIN dbo.CollectionCipher CC ON CC.CollectionId = C.Id + INNER JOIN dbo.Cipher Cipher ON Cipher.Id = CC.CipherId + AND Cipher.OrganizationId = @OrganizationId +WHERE OU.Status in (0,1,2) + AND Cipher.DeletedDate IS NULL + +UNION ALL + +-- Group-based collection permissions +SELECT + U.Id AS UserGuid, + U.Name AS UserName, + U.Email, + U.TwoFactorProviders, + U.UsesKeyConnector, + OU.ResetPasswordKey, + CC.CollectionId, + C.Name AS CollectionName, + G.Id AS GroupId, + G.Name AS GroupName, + CG.ReadOnly, + CG.HidePasswords, + CG.Manage, + Cipher.Id AS CipherId +FROM dbo.OrganizationUser OU + INNER JOIN dbo.[User] U ON U.Id = OU.UserId + INNER JOIN dbo.Organization O ON O.Id = OU.OrganizationId + AND O.Id = @OrganizationId + AND O.Enabled = 1 + INNER JOIN dbo.GroupUser GU ON GU.OrganizationUserId = OU.Id + INNER JOIN dbo.[Group] G ON G.Id = GU.GroupId + INNER JOIN dbo.CollectionGroup CG ON CG.GroupId = G.Id + INNER JOIN dbo.Collection C ON C.Id = CG.CollectionId + INNER JOIN dbo.CollectionCipher CC ON CC.CollectionId = C.Id + INNER JOIN dbo.Cipher Cipher ON Cipher.Id = CC.CipherId + AND Cipher.OrganizationId = @OrganizationId +WHERE OU.Status in (0,1,2) + AND Cipher.DeletedDate IS NULL + +GO diff --git a/util/Migrator/DbScripts/2025-06-12_00_AlterMemberAccessReportStoreProcedure.sql b/util/Migrator/DbScripts/2025-06-12_00_AlterMemberAccessReportStoreProcedure.sql new file mode 100644 index 0000000000..fb3b842fc6 --- /dev/null +++ b/util/Migrator/DbScripts/2025-06-12_00_AlterMemberAccessReportStoreProcedure.sql @@ -0,0 +1,94 @@ +CREATE OR ALTER PROC dbo.MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId + @OrganizationId UNIQUEIDENTIFIER +AS + SET NOCOUNT ON; + + IF @OrganizationId IS NULL + THROW 50000, 'OrganizationId cannot be null', 1; + + SELECT + OU.Id AS UserGuid, + U.Name AS UserName, + ISNULL(U.Email, OU.Email) as 'Email', + U.TwoFactorProviders, + U.UsesKeyConnector, + OU.ResetPasswordKey, + CC.CollectionId, + C.Name AS CollectionName, + NULL AS GroupId, + NULL AS GroupName, + CU.ReadOnly, + CU.HidePasswords, + CU.Manage, + Cipher.Id AS CipherId + FROM dbo.OrganizationUser OU + LEFT JOIN dbo.[User] U ON U.Id = OU.UserId + INNER JOIN dbo.Organization O ON O.Id = OU.OrganizationId + AND O.Id = @OrganizationId + AND O.Enabled = 1 + INNER JOIN dbo.CollectionUser CU ON CU.OrganizationUserId = OU.Id + INNER JOIN dbo.Collection C ON C.Id = CU.CollectionId and C.OrganizationId = @OrganizationId + INNER JOIN dbo.CollectionCipher CC ON CC.CollectionId = C.Id + INNER JOIN dbo.Cipher Cipher ON Cipher.Id = CC.CipherId AND Cipher.OrganizationId = @OrganizationId + WHERE OU.Status IN (0,1,2) -- Invited, Accepted and Confirmed Users + AND Cipher.DeletedDate IS NULL +UNION ALL + -- Group-based collection permissions + SELECT + OU.Id AS UserGuid, + U.Name AS UserName, + ISNULL(U.Email, OU.Email) as 'Email', + U.TwoFactorProviders, + U.UsesKeyConnector, + OU.ResetPasswordKey, + CC.CollectionId, + C.Name AS CollectionName, + G.Id AS GroupId, + G.Name AS GroupName, + CG.ReadOnly, + CG.HidePasswords, + CG.Manage, + Cipher.Id AS CipherId + FROM dbo.OrganizationUser OU + LEFT JOIN dbo.[User] U ON U.Id = OU.UserId + INNER JOIN dbo.Organization O ON O.Id = OU.OrganizationId + AND O.Id = @OrganizationId + AND O.Enabled = 1 + INNER JOIN dbo.GroupUser GU ON GU.OrganizationUserId = OU.Id + INNER JOIN dbo.[Group] G ON G.Id = GU.GroupId + INNER JOIN dbo.CollectionGroup CG ON CG.GroupId = G.Id + INNER JOIN dbo.Collection C ON C.Id = CG.CollectionId AND C.OrganizationId = @OrganizationId + INNER JOIN dbo.CollectionCipher CC ON CC.CollectionId = C.Id + INNER JOIN dbo.Cipher Cipher ON Cipher.Id = CC.CipherId and Cipher.OrganizationId = @OrganizationId + WHERE OU.Status IN (0,1,2) -- Invited, Accepted and Confirmed Users + AND Cipher.DeletedDate IS NULL +UNION ALL + -- Users without collection access (invited users) + -- typically invited users who have not yet accepted the invitation + -- and not yet assigned to any collection + SELECT + OU.Id AS UserGuid, + U.Name AS UserName, + ISNULL(U.Email, OU.Email) as 'Email', + U.TwoFactorProviders, + U.UsesKeyConnector, + OU.ResetPasswordKey, + null as CollectionId, + null AS CollectionName, + NULL AS GroupId, + NULL AS GroupName, + null as [ReadOnly], + null as HidePasswords, + null as Manage, + null AS CipherId + FROM dbo.OrganizationUser OU + LEFT JOIN dbo.[User] U ON U.Id = OU.UserId + INNER JOIN dbo.Organization O ON O.Id = OU.OrganizationId AND O.Id = @OrganizationId AND O.Enabled = 1 + WHERE OU.Status IN (0,1,2) -- Invited, Accepted and Confirmed Users + AND OU.Id not in ( + select OU1.Id from dbo.OrganizationUser OU1 + inner join dbo.CollectionUser CU1 on CU1.OrganizationUserId = OU1.Id + WHERE OU1.OrganizationId = @organizationId + ) + +GO diff --git a/util/MySqlMigrations/Migrations/20250609182150_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.Designer.cs b/util/MySqlMigrations/Migrations/20250609182150_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.Designer.cs new file mode 100644 index 0000000000..bd813ae927 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250609182150_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.Designer.cs @@ -0,0 +1,3166 @@ +// +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("20250609182150_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql")] + partial class _20250609_00_AddMemberAccessReportStoreProceduresql + { + /// + 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("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.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("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.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("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.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.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") + .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/20250609182150_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.cs b/util/MySqlMigrations/Migrations/20250609182150_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.cs new file mode 100644 index 0000000000..2c63b0bcfa --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250609182150_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class _20250609_00_AddMemberAccessReportStoreProceduresql : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OrganizationMemberBaseDetails", + columns: table => new + { + UserGuid = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + UserName = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Email = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + TwoFactorProviders = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + UsesKeyConnector = table.Column(type: "tinyint(1)", nullable: false), + ResetPasswordKey = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + CollectionId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + GroupId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + GroupName = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + CollectionName = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ReadOnly = table.Column(type: "tinyint(1)", nullable: true), + HidePasswords = table.Column(type: "tinyint(1)", nullable: true), + Manage = table.Column(type: "tinyint(1)", nullable: true), + CipherId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci") + }, + constraints: table => + { + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrganizationMemberBaseDetails"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index a22b5baf85..c4b41921b1 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -22,6 +22,53 @@ namespace Bit.MySqlMigrations.Migrations 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") @@ -920,6 +967,34 @@ namespace Bit.MySqlMigrations.Migrations b.ToTable("ProviderPlan", (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") @@ -2023,34 +2098,6 @@ namespace Bit.MySqlMigrations.Migrations 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") @@ -2532,6 +2579,17 @@ namespace Bit.MySqlMigrations.Migrations b.Navigation("Provider"); }); + 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") @@ -2825,17 +2883,6 @@ namespace Bit.MySqlMigrations.Migrations 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") diff --git a/util/PostgresMigrations/Migrations/20250609182157_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.Designer.cs b/util/PostgresMigrations/Migrations/20250609182157_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.Designer.cs new file mode 100644 index 0000000000..2056f50c54 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250609182157_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.Designer.cs @@ -0,0 +1,3172 @@ +// +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("20250609182157_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql")] + partial class _20250609_00_AddMemberAccessReportStoreProceduresql + { + /// + 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("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.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("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.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("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.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.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") + .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/20250609182157_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.cs b/util/PostgresMigrations/Migrations/20250609182157_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.cs new file mode 100644 index 0000000000..bd765900f6 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250609182157_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class _20250609_00_AddMemberAccessReportStoreProceduresql : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OrganizationMemberBaseDetails", + columns: table => new + { + UserGuid = table.Column(type: "uuid", nullable: true), + UserName = table.Column(type: "text", nullable: true), + Email = table.Column(type: "text", nullable: true), + TwoFactorProviders = table.Column(type: "text", nullable: true), + UsesKeyConnector = table.Column(type: "boolean", nullable: false), + ResetPasswordKey = table.Column(type: "text", nullable: true), + CollectionId = table.Column(type: "uuid", nullable: true), + GroupId = table.Column(type: "uuid", nullable: true), + GroupName = table.Column(type: "text", nullable: true), + CollectionName = table.Column(type: "text", nullable: true), + ReadOnly = table.Column(type: "boolean", nullable: true), + HidePasswords = table.Column(type: "boolean", nullable: true), + Manage = table.Column(type: "boolean", nullable: true), + CipherId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrganizationMemberBaseDetails"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index a016e9f350..c4dcce5070 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -23,6 +23,53 @@ namespace Bit.PostgresMigrations.Migrations 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") @@ -925,6 +972,34 @@ namespace Bit.PostgresMigrations.Migrations b.ToTable("ProviderPlan", (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") @@ -2029,34 +2104,6 @@ namespace Bit.PostgresMigrations.Migrations 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") @@ -2538,6 +2585,17 @@ namespace Bit.PostgresMigrations.Migrations b.Navigation("Provider"); }); + 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") @@ -2831,17 +2889,6 @@ namespace Bit.PostgresMigrations.Migrations 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") diff --git a/util/SqliteMigrations/Migrations/20250609182153_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.Designer.cs b/util/SqliteMigrations/Migrations/20250609182153_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.Designer.cs new file mode 100644 index 0000000000..b6bb26bbe5 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250609182153_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.Designer.cs @@ -0,0 +1,3155 @@ +// +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("20250609182153_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql")] + partial class _20250609_00_AddMemberAccessReportStoreProceduresql + { + /// + 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("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.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("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.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("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.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.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") + .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/20250609182153_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.cs b/util/SqliteMigrations/Migrations/20250609182153_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.cs new file mode 100644 index 0000000000..caace2ca13 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250609182153_2025-06-09_00_AddMemberAccessReportStoreProcedure.sql.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class _20250609_00_AddMemberAccessReportStoreProceduresql : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OrganizationMemberBaseDetails", + columns: table => new + { + UserGuid = table.Column(type: "TEXT", nullable: true), + UserName = table.Column(type: "TEXT", nullable: true), + Email = table.Column(type: "TEXT", nullable: true), + TwoFactorProviders = table.Column(type: "TEXT", nullable: true), + UsesKeyConnector = table.Column(type: "INTEGER", nullable: false), + ResetPasswordKey = table.Column(type: "TEXT", nullable: true), + CollectionId = table.Column(type: "TEXT", nullable: true), + GroupId = table.Column(type: "TEXT", nullable: true), + GroupName = table.Column(type: "TEXT", nullable: true), + CollectionName = table.Column(type: "TEXT", nullable: true), + ReadOnly = table.Column(type: "INTEGER", nullable: true), + HidePasswords = table.Column(type: "INTEGER", nullable: true), + Manage = table.Column(type: "INTEGER", nullable: true), + CipherId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrganizationMemberBaseDetails"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index b730831439..611e042a1f 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -17,6 +17,53 @@ namespace Bit.SqliteMigrations.Migrations #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") @@ -909,6 +956,34 @@ namespace Bit.SqliteMigrations.Migrations b.ToTable("ProviderPlan", (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") @@ -2012,34 +2087,6 @@ namespace Bit.SqliteMigrations.Migrations 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") @@ -2521,6 +2568,17 @@ namespace Bit.SqliteMigrations.Migrations b.Navigation("Provider"); }); + 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") @@ -2814,17 +2872,6 @@ namespace Bit.SqliteMigrations.Migrations 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") From 5ffa9379146e1daf0aa25a538f1f565fe4639036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 17 Jun 2025 12:20:22 +0100 Subject: [PATCH 04/47] [PM-22098] Create default collection when organization member is confirmed (#5944) * Add RequiresDefaultCollection method to PersonalOwnershipPolicyRequirement * Add CreateDefaultLocation feature flag to Constants.cs * Add DefaultUserCollectionName property to OrganizationUserConfirmRequestModel with encryption attributes * Update PersonalOwnershipPolicyRequirement instantiation in tests to use constructor with parameters instead of property assignment * Enhance ConfirmOrganizationUserCommand to support default user collection creation. Added logic to check if a default collection is required based on organization policies and feature flags. Updated ConfirmUserAsync method signature to include an optional defaultUserCollectionName parameter. Added corresponding tests to validate the new functionality. * Refactor Confirm method in OrganizationUsersController to use Guid parameters directly, simplifying the code. Updated ConfirmUserAsync call to include DefaultUserCollectionName from the input model. * Move logic for handling confirmation side effects into a separate method * Refactor PersonalOwnershipPolicyRequirement to use enum for ownership state - Introduced PersonalOwnershipState enum to represent allowed and restricted states. - Updated PersonalOwnershipPolicyRequirement constructor and properties to utilize the new enum. - Modified related classes and tests to reflect changes in ownership state handling. --- .../OrganizationUsersController.cs | 7 +- .../OrganizationUserRequestModels.cs | 4 + .../ConfirmOrganizationUserCommand.cs | 61 +++++++++++- .../IConfirmOrganizationUserCommand.cs | 3 +- .../PersonalOwnershipPolicyRequirement.cs | 54 ++++++++++- src/Core/Constants.cs | 1 + .../ImportFeatures/ImportCiphersCommand.cs | 2 +- .../Services/Implementations/CipherService.cs | 2 +- .../ConfirmOrganizationUserCommandTests.cs | 95 +++++++++++++++++++ ...lOwnershipPolicyRequirementFactoryTests.cs | 30 +++++- .../ImportCiphersAsyncCommandTests.cs | 8 +- .../Vault/Services/CipherServiceTests.cs | 8 +- 12 files changed, 254 insertions(+), 21 deletions(-) diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 1b58fcfebe..7765eb2665 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -403,16 +403,15 @@ public class OrganizationUsersController : Controller } [HttpPost("{id}/confirm")] - public async Task Confirm(string orgId, string id, [FromBody] OrganizationUserConfirmRequestModel model) + public async Task Confirm(Guid orgId, Guid id, [FromBody] OrganizationUserConfirmRequestModel model) { - var orgGuidId = new Guid(orgId); - if (!await _currentContext.ManageUsers(orgGuidId)) + if (!await _currentContext.ManageUsers(orgId)) { throw new NotFoundException(); } var userId = _userService.GetProperUserId(User); - var result = await _confirmOrganizationUserCommand.ConfirmUserAsync(orgGuidId, new Guid(id), model.Key, userId.Value); + var result = await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, id, model.Key, userId.Value, model.DefaultUserCollectionName); } [HttpPost("confirm")] diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRequestModels.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRequestModels.cs index bbbb571f42..e6d4f85d3b 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRequestModels.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRequestModels.cs @@ -60,6 +60,10 @@ public class OrganizationUserConfirmRequestModel { [Required] public string Key { get; set; } + + [EncryptedString] + [EncryptedStringLength(1000)] + public string DefaultUserCollectionName { get; set; } } public class OrganizationUserBulkConfirmRequestModelEntry diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs index 806cf5a533..dd118d7ea3 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs @@ -8,6 +8,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; @@ -28,6 +29,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand private readonly IDeviceRepository _deviceRepository; private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IFeatureService _featureService; + private readonly ICollectionRepository _collectionRepository; public ConfirmOrganizationUserCommand( IOrganizationRepository organizationRepository, @@ -41,7 +43,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand IPolicyService policyService, IDeviceRepository deviceRepository, IPolicyRequirementQuery policyRequirementQuery, - IFeatureService featureService) + IFeatureService featureService, + ICollectionRepository collectionRepository) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -55,10 +58,11 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand _deviceRepository = deviceRepository; _policyRequirementQuery = policyRequirementQuery; _featureService = featureService; + _collectionRepository = collectionRepository; } public async Task ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, - Guid confirmingUserId) + Guid confirmingUserId, string defaultUserCollectionName = null) { var result = await ConfirmUsersAsync( organizationId, @@ -75,6 +79,9 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand { throw new BadRequestException(error); } + + await HandleConfirmationSideEffectsAsync(organizationId, orgUser, defaultUserCollectionName); + return orgUser; } @@ -213,4 +220,54 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand .Where(d => !string.IsNullOrWhiteSpace(d.PushToken)) .Select(d => d.Id.ToString()); } + + private async Task HandleConfirmationSideEffectsAsync(Guid organizationId, OrganizationUser organizationUser, string defaultUserCollectionName) + { + // Create DefaultUserCollection type collection for the user if the PersonalOwnership 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) + { + if (!_featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)) + { + return false; + } + + // Skip if no collection name provided (backwards compatibility) + if (string.IsNullOrWhiteSpace(defaultUserCollectionName)) + { + return false; + } + + var personalOwnershipRequirement = await _policyRequirementQuery.GetAsync(userId); + return personalOwnershipRequirement.RequiresDefaultCollection(organizationId); + } + + private async Task CreateDefaultCollectionAsync(Guid organizationId, Guid organizationUserId, string defaultCollectionName) + { + var collection = new Collection + { + OrganizationId = organizationId, + Name = defaultCollectionName, + Type = CollectionType.DefaultUserCollection + }; + + var userAccess = new List + { + new CollectionAccessSelection + { + Id = organizationUserId, + ReadOnly = false, + HidePasswords = false, + Manage = true + } + }; + + await _collectionRepository.CreateAsync(collection, groups: null, users: userAccess); + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs index e574d29e48..734b8d2b0c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs @@ -15,9 +15,10 @@ public interface IConfirmOrganizationUserCommand /// The ID of the organization user to confirm. /// The encrypted organization key for the user. /// The ID of the user performing the confirmation. + /// Optional encrypted collection name for creating a default collection. /// The confirmed organization user. /// Thrown when the user is not valid or cannot be confirmed. - Task ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId); + Task ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId, string defaultUserCollectionName = null); /// /// Confirms multiple organization users who have accepted their invitations. diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs index 6f3f017bb9..219d3f1bf8 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs @@ -3,15 +3,55 @@ using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +/// +/// Represents the personal ownership policy state. +/// +public enum PersonalOwnershipState +{ + /// + /// Personal ownership is allowed - users can save items to their personal vault. + /// + Allowed, + + /// + /// Personal ownership is restricted - members are required to save items to an organization. + /// + Restricted +} + /// /// Policy requirements for the Disable Personal Ownership policy. /// public class PersonalOwnershipPolicyRequirement : IPolicyRequirement { + private readonly IEnumerable _organizationIdsWithPolicyEnabled; + + /// + /// The personal ownership state for the user. + /// + /// + /// The collection of Organization IDs that have the Disable Personal Ownership policy enabled. + /// + public PersonalOwnershipPolicyRequirement( + PersonalOwnershipState personalOwnershipState, + IEnumerable organizationIdsWithPolicyEnabled) + { + _organizationIdsWithPolicyEnabled = organizationIdsWithPolicyEnabled ?? []; + State = personalOwnershipState; + } + /// - /// Indicates whether Personal Ownership is disabled for the user. If true, members are required to save items to an organization. + /// The personal ownership policy state for the user. /// - public bool DisablePersonalOwnership { get; init; } + public PersonalOwnershipState State { get; } + + /// + /// Returns true if the Disable Personal Ownership policy is enforced in that organization. + /// + public bool RequiresDefaultCollection(Guid organizationId) + { + return _organizationIdsWithPolicyEnabled.Contains(organizationId); + } } public class PersonalOwnershipPolicyRequirementFactory : BasePolicyRequirementFactory @@ -20,7 +60,13 @@ public class PersonalOwnershipPolicyRequirementFactory : BasePolicyRequirementFa public override PersonalOwnershipPolicyRequirement Create(IEnumerable policyDetails) { - var result = new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = policyDetails.Any() }; - return result; + var personalOwnershipState = policyDetails.Any() + ? PersonalOwnershipState.Restricted + : PersonalOwnershipState.Allowed; + var organizationIdsWithPolicyEnabled = policyDetails.Select(p => p.OrganizationId).ToHashSet(); + + return new PersonalOwnershipPolicyRequirement( + personalOwnershipState, + organizationIdsWithPolicyEnabled); } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 49d360ec50..09c4294cd6 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -112,6 +112,7 @@ public static class FeatureFlagKeys 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"; /* Auth Team */ public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence"; diff --git a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs index f67a2550d2..9fc6238143 100644 --- a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs +++ b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs @@ -56,7 +56,7 @@ public class ImportCiphersCommand : IImportCiphersCommand { // Make sure the user can save new ciphers to their personal vault var isPersonalVaultRestricted = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements) - ? (await _policyRequirementQuery.GetAsync(importingUserId)).DisablePersonalOwnership + ? (await _policyRequirementQuery.GetAsync(importingUserId)).State == PersonalOwnershipState.Restricted : await _policyService.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.PersonalOwnership); if (isPersonalVaultRestricted) diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index 5d17441024..5fa27039c2 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -143,7 +143,7 @@ public class CipherService : ICipherService else { var isPersonalVaultRestricted = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements) - ? (await _policyRequirementQuery.GetAsync(savingUserId)).DisablePersonalOwnership + ? (await _policyRequirementQuery.GetAsync(savingUserId)).State == PersonalOwnershipState.Restricted : await _policyService.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership); if (isPersonalVaultRestricted) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs index 366d8cb2d6..1732f61d5e 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs @@ -10,6 +10,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.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; @@ -442,4 +443,98 @@ public class ConfirmOrganizationUserCommandTests await sutProvider.GetDependency().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, orgUser.AccessSecretsManager); await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is>(users => users.Contains(orgUser) && users.Count == 1)); } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithPersonalOwnershipPolicyApplicable_WithValidCollectionName_CreatesDefaultCollection( + Organization organization, OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, + string key, string collectionName, SutProvider sutProvider) + { + organization.PlanType = PlanType.EnterpriseAnnually; + orgUser.OrganizationId = confirmingUser.OrganizationId = organization.Id; + orgUser.UserId = user.Id; + + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); + sutProvider.GetDependency().GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); + + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true); + + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new PersonalOwnershipPolicyRequirement( + PersonalOwnershipState.Restricted, + [organization.Id])); + + await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName); + + 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)); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithPersonalOwnershipPolicyApplicable_WithInvalidCollectionName_DoesNotCreateDefaultCollection( + Organization org, OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, + string key, SutProvider sutProvider) + { + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; + orgUser.UserId = user.Id; + + sutProvider.GetDependency().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + sutProvider.GetDependency().GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); + + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true); + + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new PersonalOwnershipPolicyRequirement( + PersonalOwnershipState.Restricted, + [org.Id])); + + await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, ""); + + await sutProvider.GetDependency() + .DidNotReceive() + .CreateAsync(Arg.Any(), Arg.Any>(), Arg.Any>()); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithPersonalOwnershipPolicyNotApplicable_DoesNotCreateDefaultCollection( + Organization org, OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, + string key, string collectionName, SutProvider sutProvider) + { + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; + orgUser.UserId = user.Id; + + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + sutProvider.GetDependency().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); + sutProvider.GetDependency().GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true); + + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new PersonalOwnershipPolicyRequirement( + PersonalOwnershipState.Restricted, + [Guid.NewGuid()])); + + await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName); + + await sutProvider.GetDependency() + .DidNotReceive() + .CreateAsync(Arg.Any(), Arg.Any>(), Arg.Any>()); + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirementFactoryTests.cs index 2ce75ca61e..dfac394243 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirementFactoryTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirementFactoryTests.cs @@ -12,20 +12,42 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequire public class PersonalOwnershipPolicyRequirementFactoryTests { [Theory, BitAutoData] - public void DisablePersonalOwnership_WithNoPolicies_ReturnsFalse(SutProvider sutProvider) + public void State_WithNoPolicies_ReturnsAllowed(SutProvider sutProvider) { var actual = sutProvider.Sut.Create([]); - Assert.False(actual.DisablePersonalOwnership); + Assert.Equal(PersonalOwnershipState.Allowed, actual.State); } [Theory, BitAutoData] - public void DisablePersonalOwnership_WithPersonalOwnershipPolicies_ReturnsTrue( + public void State_WithPersonalOwnershipPolicies_ReturnsRestricted( [PolicyDetails(PolicyType.PersonalOwnership)] PolicyDetails[] policies, SutProvider sutProvider) { var actual = sutProvider.Sut.Create(policies); - Assert.True(actual.DisablePersonalOwnership); + Assert.Equal(PersonalOwnershipState.Restricted, actual.State); + } + + [Theory, BitAutoData] + public void RequiresDefaultCollection_WithNoPolicies_ReturnsFalse( + Guid organizationId, + SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create([]); + + Assert.False(actual.RequiresDefaultCollection(organizationId)); + } + + [Theory, BitAutoData] + public void RequiresDefaultCollection_WithPersonalOwnershipPolicies_ReturnsCorrectResult( + [PolicyDetails(PolicyType.PersonalOwnership)] PolicyDetails[] policies, + Guid nonPolicyOrganizationId, + SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create(policies); + + Assert.True(actual.RequiresDefaultCollection(policies[0].OrganizationId)); + Assert.False(actual.RequiresDefaultCollection(nonPolicyOrganizationId)); } } diff --git a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs index 5605b5ab2a..f61c2f4443 100644 --- a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs +++ b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs @@ -62,7 +62,9 @@ public class ImportCiphersAsyncCommandTests sutProvider.GetDependency() .GetAsync(importingUserId) - .Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = false }); + .Returns(new PersonalOwnershipPolicyRequirement( + PersonalOwnershipState.Allowed, + [])); sutProvider.GetDependency() .GetManyByUserIdAsync(importingUserId) @@ -116,7 +118,9 @@ public class ImportCiphersAsyncCommandTests sutProvider.GetDependency() .GetAsync(userId) - .Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = true }); + .Returns(new PersonalOwnershipPolicyRequirement( + PersonalOwnershipState.Restricted, + [Guid.NewGuid()])); var folderRelationships = new List>(); diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs index 0941372963..1bd41e9f01 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -171,7 +171,9 @@ public class CipherServiceTests sutProvider.GetDependency() .GetAsync(savingUserId) - .Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = true }); + .Returns(new PersonalOwnershipPolicyRequirement( + PersonalOwnershipState.Restricted, + [Guid.NewGuid()])); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null)); @@ -195,7 +197,9 @@ public class CipherServiceTests sutProvider.GetDependency() .GetAsync(savingUserId) - .Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = false }); + .Returns(new PersonalOwnershipPolicyRequirement( + PersonalOwnershipState.Allowed, + [])); await sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null); From a529492d1d0eb3e09bc1539ad1c64798eeeb0cfa Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Tue, 17 Jun 2025 08:38:16 -0400 Subject: [PATCH 05/47] [PM-17562] Update documentation for event integrations (#5924) * [PM-17562] Update documentation for event integrations * Fix SonarQube suggestion, bring ASB event listener in line with integration listener * Apply suggestions from code review Co-authored-by: Matt Bishop * Updates to README - PR fixes, additional context, tense alignment * Fix the formatting for inlined code snippets * Add links to different sections; remove inline code formatting in favor of single bacticks for JSON --------- Co-authored-by: Matt Bishop --- .../Controllers/SlackIntegrationController.cs | 2 +- ...ionIntegrationConfigurationRequestModel.cs | 2 +- .../IIntegrationMessage.cs | 2 +- .../IntegrationHandlerResult.cs | 2 +- .../IntegrationMessage.cs | 2 +- .../IntegrationTemplateContext.cs | 2 +- .../SlackIntegration.cs | 2 +- .../SlackIntegrationConfiguration.cs | 2 +- .../SlackIntegrationConfigurationDetails.cs | 2 +- .../WebhookIntegrationConfiguration.cs | 2 +- .../WebhookIntegrationConfigurationDetails.cs | 2 +- .../Services/IAzureServiceBusService.cs | 2 +- .../Services/IEventIntegrationPublisher.cs | 2 +- .../Services/IIntegrationHandler.cs | 2 +- .../AdminConsole/Services/IRabbitMqService.cs | 2 +- .../AzureServiceBusEventListenerService.cs | 19 +- ...ureServiceBusIntegrationListenerService.cs | 0 .../AzureServiceBusService.cs | 2 +- .../EventIntegrationEventWriteService.cs | 0 .../EventIntegrationHandler.cs | 2 +- .../EventRepositoryHandler.cs | 0 .../EventRouteService.cs | 0 .../EventIntegrations/README.md | 375 ++++++++++++++++++ .../RabbitMqEventListenerService.cs | 0 .../RabbitMqIntegrationListenerService.cs | 2 +- .../RabbitMqService.cs | 2 +- .../SlackIntegrationHandler.cs | 2 +- .../{ => EventIntegrations}/SlackService.cs | 0 .../WebhookIntegrationHandler.cs | 2 +- .../Utilities/ServiceCollectionExtensions.cs | 2 +- ...ntegrationsConfigurationControllerTests.cs | 2 +- ...tegrationConfigurationRequestModelTests.cs | 2 +- .../IntegrationMessageTests.cs | 5 +- ...rviceBusIntegrationListenerServiceTests.cs | 2 +- .../Services/EventIntegrationHandlerTests.cs | 2 +- .../Services/IntegrationHandlerTests.cs | 2 +- ...RabbitMqIntegrationListenerServiceTests.cs | 2 +- .../Services/SlackIntegrationHandlerTests.cs | 2 +- .../WebhookIntegrationHandlerTests.cs | 2 +- 39 files changed, 415 insertions(+), 44 deletions(-) rename src/Core/AdminConsole/Models/Data/{Integrations => EventIntegrations}/IIntegrationMessage.cs (82%) rename src/Core/AdminConsole/Models/Data/{Integrations => EventIntegrations}/IntegrationHandlerResult.cs (88%) rename src/Core/AdminConsole/Models/Data/{Integrations => EventIntegrations}/IntegrationMessage.cs (94%) rename src/Core/AdminConsole/Models/Data/{Integrations => EventIntegrations}/IntegrationTemplateContext.cs (95%) rename src/Core/AdminConsole/Models/Data/{Integrations => EventIntegrations}/SlackIntegration.cs (51%) rename src/Core/AdminConsole/Models/Data/{Integrations => EventIntegrations}/SlackIntegrationConfiguration.cs (57%) rename src/Core/AdminConsole/Models/Data/{Integrations => EventIntegrations}/SlackIntegrationConfigurationDetails.cs (62%) rename src/Core/AdminConsole/Models/Data/{Integrations => EventIntegrations}/WebhookIntegrationConfiguration.cs (56%) rename src/Core/AdminConsole/Models/Data/{Integrations => EventIntegrations}/WebhookIntegrationConfigurationDetails.cs (58%) rename src/Core/AdminConsole/Services/Implementations/{ => EventIntegrations}/AzureServiceBusEventListenerService.cs (93%) rename src/Core/AdminConsole/Services/Implementations/{ => EventIntegrations}/AzureServiceBusIntegrationListenerService.cs (100%) rename src/Core/AdminConsole/Services/Implementations/{ => EventIntegrations}/AzureServiceBusService.cs (97%) rename src/Core/AdminConsole/Services/Implementations/{ => EventIntegrations}/EventIntegrationEventWriteService.cs (100%) rename src/Core/AdminConsole/Services/Implementations/{ => EventIntegrations}/EventIntegrationHandler.cs (98%) rename src/Core/AdminConsole/Services/Implementations/{ => EventIntegrations}/EventRepositoryHandler.cs (100%) rename src/Core/AdminConsole/Services/Implementations/{ => EventIntegrations}/EventRouteService.cs (100%) create mode 100644 src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md rename src/Core/AdminConsole/Services/Implementations/{ => EventIntegrations}/RabbitMqEventListenerService.cs (100%) rename src/Core/AdminConsole/Services/Implementations/{ => EventIntegrations}/RabbitMqIntegrationListenerService.cs (98%) rename src/Core/AdminConsole/Services/Implementations/{ => EventIntegrations}/RabbitMqService.cs (99%) rename src/Core/AdminConsole/Services/Implementations/{ => EventIntegrations}/SlackIntegrationHandler.cs (91%) rename src/Core/AdminConsole/Services/Implementations/{ => EventIntegrations}/SlackService.cs (100%) rename src/Core/AdminConsole/Services/Implementations/{ => EventIntegrations}/WebhookIntegrationHandler.cs (97%) rename test/Core.Test/AdminConsole/Models/Data/{Integrations => EventIntegrations}/IntegrationMessageTests.cs (95%) diff --git a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs b/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs index c0ab5c059b..3d749d25d7 100644 --- a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs +++ b/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs @@ -2,7 +2,7 @@ using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Core; using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs index ccab2b36ae..85d46c791b 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json; using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Enums; #nullable enable diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationMessage.cs similarity index 82% rename from src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs rename to src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationMessage.cs index c94794765b..f979b8af0e 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationMessage.cs @@ -2,7 +2,7 @@ using Bit.Core.Enums; -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public interface IIntegrationMessage { diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResult.cs similarity index 88% rename from src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs rename to src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResult.cs index ecf5d25c51..d3b0c0d5ac 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResult.cs @@ -1,6 +1,6 @@ #nullable enable -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public class IntegrationHandlerResult { diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationMessage.cs similarity index 94% rename from src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs rename to src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationMessage.cs index 018d453cb9..1861ec4522 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationMessage.cs @@ -3,7 +3,7 @@ using System.Text.Json; using Bit.Core.Enums; -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public class IntegrationMessage : IIntegrationMessage { diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs similarity index 95% rename from src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs rename to src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs index 338c2b963d..82c236865f 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs @@ -5,7 +5,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public class IntegrationTemplateContext(EventMessage eventMessage) { diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs similarity index 51% rename from src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs rename to src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs index 4f2c434ff6..1e6fbadb34 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs @@ -1,5 +1,5 @@ #nullable enable -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public record SlackIntegration(string token); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs similarity index 57% rename from src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs rename to src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs index 18b13248ec..13d1ad168f 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs @@ -1,5 +1,5 @@ #nullable enable -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public record SlackIntegrationConfiguration(string channelId); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs similarity index 62% rename from src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs rename to src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs index a9b4150419..219149e4f9 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs @@ -1,5 +1,5 @@ #nullable enable -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public record SlackIntegrationConfigurationDetails(string channelId, string token); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs similarity index 56% rename from src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs rename to src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs index 47e014ee2a..5d00778e4b 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs @@ -1,5 +1,5 @@ #nullable enable -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public record WebhookIntegrationConfiguration(string url); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs similarity index 58% rename from src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs rename to src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs index c4c41db24f..4790588c32 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs @@ -1,5 +1,5 @@ #nullable enable -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public record WebhookIntegrationConfigurationDetails(string url); diff --git a/src/Core/AdminConsole/Services/IAzureServiceBusService.cs b/src/Core/AdminConsole/Services/IAzureServiceBusService.cs index d254e763d5..75864255c2 100644 --- a/src/Core/AdminConsole/Services/IAzureServiceBusService.cs +++ b/src/Core/AdminConsole/Services/IAzureServiceBusService.cs @@ -1,5 +1,5 @@ using Azure.Messaging.ServiceBus; -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs b/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs index 560da576b7..b80b518223 100644 --- a/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs +++ b/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/IIntegrationHandler.cs b/src/Core/AdminConsole/Services/IIntegrationHandler.cs index bf6e6791cf..e02f26a873 100644 --- a/src/Core/AdminConsole/Services/IIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/IIntegrationHandler.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/IRabbitMqService.cs b/src/Core/AdminConsole/Services/IRabbitMqService.cs index b0b9a72eac..12c40c3b98 100644 --- a/src/Core/AdminConsole/Services/IRabbitMqService.cs +++ b/src/Core/AdminConsole/Services/IRabbitMqService.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using RabbitMQ.Client; using RabbitMQ.Client.Events; diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs similarity index 93% rename from src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs rename to src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs index 8b00204775..ffa148fc08 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs @@ -33,6 +33,13 @@ public class AzureServiceBusEventListenerService : EventLoggingListenerService await _processor.StartProcessingAsync(cancellationToken); } + public override async Task StopAsync(CancellationToken cancellationToken) + { + await _processor.StopProcessingAsync(cancellationToken); + await _processor.DisposeAsync(); + await base.StopAsync(cancellationToken); + } + internal Task ProcessErrorAsync(ProcessErrorEventArgs args) { _logger.LogError( @@ -49,16 +56,4 @@ public class AzureServiceBusEventListenerService : EventLoggingListenerService await ProcessReceivedMessageAsync(Encoding.UTF8.GetString(args.Message.Body), args.Message.MessageId); await args.CompleteMessageAsync(args.Message); } - - public override async Task StopAsync(CancellationToken cancellationToken) - { - await _processor.StopProcessingAsync(cancellationToken); - await base.StopAsync(cancellationToken); - } - - public override void Dispose() - { - _processor.DisposeAsync().GetAwaiter().GetResult(); - base.Dispose(); - } } diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs similarity index 100% rename from src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs rename to src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusService.cs similarity index 97% rename from src/Core/AdminConsole/Services/Implementations/AzureServiceBusService.cs rename to src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusService.cs index 7d24095819..4887aa3a7f 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusService.cs @@ -1,5 +1,5 @@ using Azure.Messaging.ServiceBus; -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Enums; using Bit.Core.Settings; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrationEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationEventWriteService.cs similarity index 100% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrationEventWriteService.cs rename to src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationEventWriteService.cs diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs similarity index 98% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs rename to src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs index aa76fdf8bc..e728d7c5c0 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs @@ -1,7 +1,7 @@ #nullable enable using System.Text.Json; -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.Utilities; using Bit.Core.Enums; using Bit.Core.Models.Data; diff --git a/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRepositoryHandler.cs similarity index 100% rename from src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs rename to src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRepositoryHandler.cs diff --git a/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRouteService.cs similarity index 100% rename from src/Core/AdminConsole/Services/Implementations/EventRouteService.cs rename to src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRouteService.cs diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md new file mode 100644 index 0000000000..54b36d2595 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md @@ -0,0 +1,375 @@ +# Design goals + +The main goal of event integrations is to easily enable adding new integrations over time without the need +for a lot of custom work to expose events to a new integration. The ability of fan-out offered by AMQP +(either in RabbitMQ or in Azure Service Bus) gives us a way to attach any number of new integrations to the +existing event system without needing to add special handling. By adding a new listener to the existing +pipeline, it gains an independent stream of events without the need for additional broadcast code. + +We want to enable robust handling of failures and retries. By utilizing the two-tier approach +([described below](#two-tier-exchange)), we build in support at the service level for retries. When we add +new integrations, they can focus solely on the integration-specific logic and reporting status, with all the +process of retries and delays managed by the messaging system. + +Another goal is to not only support this functionality in the cloud version, but offer it as well to +self-hosted instances. RabbitMQ provides a lightweight way for self-hosted instances to tie into the event system +using the same robust architecture for integrations without the need for Azure Service Bus. + +Finally, we want to offer organization admins flexibility and control over what events are significant, where +to send events, and the data to be included in the message. The configuration architecture allows Organizations +to customize details of a specific integration; see [Integrations and integration +configurations](#integrations-and-integration-configurations) below for more details on the configuration piece. + +# Architecture + +The entry point for the event integrations is the `IEventWriteService`. By configuring the +`EventIntegrationEventWriteService` as the `EventWriteService`, all events sent to the +service are broadcast on the RabbitMQ or Azure Service Bus message exchange. To abstract away +the specifics of publishing to a specific AMQP provider, an `IEventIntegrationPublisher` +is injected into `EventIntegrationEventWriteService` to handle the publishing of events to the +RabbitMQ or Azure Service Bus service. + +## Two-tier exchange + +When `EventIntegrationEventWriteService` publishes, it posts to the first tier of our two-tier +approach to handling messages. Each tier is represented in the AMQP stack by a separate exchange +(in RabbitMQ terminology) or topic (in Azure Service Bus). + +``` mermaid +flowchart TD + B1[EventService] + B2[EventIntegrationEventWriteService] + B3[Event Exchange / Topic] + B4[EventRepositoryHandler] + B5[WebhookIntegrationHandler] + B6[Events in Database / Azure Tables] + B7[HTTP Server] + B8[SlackIntegrationHandler] + B9[Slack] + B10[EventIntegrationHandler] + B12[Integration Exchange / Topic] + + B1 -->|IEventWriteService| B2 --> B3 + B3-->|EventListenerService| B4 --> B6 + B3-->|EventListenerService| B10 + B3-->|EventListenerService| B10 + B10 --> B12 + B12 -->|IntegrationListenerService| B5 + B12 -->|IntegrationListenerService| B8 + B5 -->|HTTP POST| B7 + B8 -->|HTTP POST| B9 +``` + +### Event tier + +In the first tier, events are broadcast in a fan-out to a series of listeners. The message body +is a JSON representation of an individual `EventMessage` or an array of `EventMessage`. Handlers at +this level are responsible for handling each event or array of events. There are currently two handlers +at this level: + - `EventRepositoryHandler` + - The `EventRepositoryHandler` is responsible for long term storage of events. It receives all events + and stores them via an injected `IEventRepository` into the database. + - This mirrors the behavior of when event integrations are turned off - cloud stores to Azure Tables + and self-hosted is stored to the database. + - `EventIntegrationHandler` + - The `EventIntegrationHandler` is a generic class that is customized to each integration (via the + configuration details of the integration) and is responsible for determining if there's a configuration + for this event / organization / integration, fetching that configuration, and parsing the details of the + event into a template string. + - The `EventIntegrationHandler` uses the injected `IOrganizationIntegrationConfigurationRepository` to pull + the specific set of configuration and template based on the event type, organization, and integration type. + This configuration is what determines if an integration should be sent, what details are necessary for sending + it, and the actual message to send. + - The output of `EventIntegrationHandler` is a new `IntegrationMessage`, with the details of this + the configuration necessary to interact with the integration and the message to send (with all the event + details incorporated), published to the integration level of the message bus. + +### Integration tier + +At the integration level, messages are JSON representations of `IIntegrationMessage` - specifically they +will be concrete types of the generic `IntegrationMessage` where `` is the configuration details of the +specific integration for which they've been sent. These messages represent the details required for +sending a specific event to a specific integration, including handling retries and delays. + +Handlers at the integration level are tied directly to the integration (e.g. `SlackIntegrationHandler`, +`WebhookIntegrationHandler`). These handlers take in `IntegrationMessage` and output +`IntegrationHandlerResult`, which tells the listener the outcome of the integration (e.g. success / fail, +if it can be retried and any minimum delay that should occur). This makes them easy to unit test in isolation +without any of the concerns of AMQP or messaging. + +The listeners at this level are responsible for firing off the handler when a new message comes in and then +taking the correct action based on the result. Successful results simply acknowledge the message and resolve. +Failures will either be sent to the dead letter queue (DLQ) or re-published for retry after the correct amount of delay. + +### Retries + +One of the goals of introducing the integration level is to simplify and enable the process of multiple retries +for a specific event integration. For instance, if a service is temporarily down, we don't want one of our handlers +blocking the rest of the queue while it waits to retry. In addition, we don't want to retry _all_ integrations for a +specific event if only one integration fails nor do we want to re-lookup the configuration details. By splitting +out the `IntegrationMessage` with the configuration, message, and details around retries, we can process each +event / integration individually and retry easily. + +When the `IntegrationHandlerResult.Success` is set to `false` (indicating that the integration attempt failed) the +`Retryable` flag tells the listener whether this failure is temporary or final. If the `Retryable` is `false`, then +the message is immediately sent to the DLQ. If it is `true`, the listener uses the `ApplyRetry(DateTime)` method +in `IntegrationMessage` which handles both incrementing the `RetryCount` and updating the `DelayUntilDate` using +the provided DateTime, but also adding exponential backoff (based on `RetryCount`) and jitter. The listener compares +the `RetryCount` in the `IntegrationMessage` to see if it's over the `MaxRetries` defined in Global Settings. If it +is over the `MaxRetries`, the message is sent to the DLQ. Otherwise, it is scheduled for retry. + +``` mermaid +flowchart TD +A[Success == false] --> B{Retryable?} + B -- No --> C[Send to Dead Letter Queue DLQ] + B -- Yes --> D[Check RetryCount vs MaxRetries] + D -->|RetryCount >= MaxRetries| E[Send to Dead Letter Queue DLQ] + D -->|RetryCount < MaxRetries| F[Schedule for Retry] +``` + +Azure Service Bus supports scheduling messages as part of its core functionality. Retries are scheduled to a specific +time and then ASB holds the message and publishes it at the correct time. + +#### RabbitMQ retry options + +For RabbitMQ (which will be used by self-host only), we have two different options. The `useDelayPlugin` flag in +`GlobalSettings.RabbitMqSettings` determines which one is used. If it is set to `true`, we use the delay plugin. It +defaults to `false` which indicates we should use retry queues with a timing check. + +1. Delay plugin + - [Delay plugin GitHub repo](https://github.com/rabbitmq/rabbitmq-delayed-message-exchange) + - This plugin enables a delayed message exchange in RabbitMQ that supports delaying a message for an amount + of time specified in a special header. + - This allows us to forego using any retry queues and rely instead on the delay exchange. When a message is + marked with the header it gets published to the exchange and the exchange handles all the functionality of + holding it until the appropriate time (similar to ASB's built-in support). + - The plugin must be setup and enabled before turning this option on (which is why it defaults to off). + +2. Retry queues + timing check + - If the delay plugin setting is off, we push the message to a retry queue which has a fixed amount of time before + it gets re-published back to the main queue. + - When a message comes off the queue, we check to see if the `DelayUntilDate` has already passed. + - If it has passed, we then handle the integration normally and retry the request. + - If it is still in the future, we put the message back on the retry queue for an additional wait. + - While this does use extra processing, it gives us better support for honoring the delays even if the delay plugin + isn't enabled. Since this solution is only intended for self-host, it should be a pretty minimal impact with short + delays and a small number of retries. + +## Listener / Handler pattern + +To make it easy to support multiple AMQP services (RabbitMQ and Azure Service Bus), the act +of listening to the stream of messages is decoupled from the act of responding to a message. + +### Listeners + +- Listeners handle the details of the communication platform (i.e. RabbitMQ and Azure Service Bus). +- There is one listener for each platform (RabbitMQ / ASB) for each of the two levels - i.e. one event listener + and one integration listener. +- Perform all the aspects of setup / teardown, subscription, message acknowledgement, etc. for the messaging platform, + but do not directly process any events themselves. Instead, they delegate to the handler with which they + are configured. +- Multiple instances can be configured to run independently, each with its own handler and + subscription / queue. + +### Handlers + +- One handler per queue / subscription (e.g. per integration at the integration level). +- Completely isolated from and know nothing of the messaging platform in use. This allows them to be + freely reused across different communication platforms. +- Perform all aspects of handling an event. +- Allows them to be highly testable as they are isolated and decoupled from the more complicated + aspects of messaging. + +This combination allows for a configuration inside of `ServiceCollectionExtensions.cs` that pairs +instances of the listener service for the currently running messaging platform with any number of +handlers. It also allows for quick development of new handlers as they are focused only on the +task of handling a specific event. + +## Publishers and Services + +Listeners (and `EventIntegrationHandler`) interact with the messaging system via the `IEventPublisher` interface, +which is backed by a RabbitMQ and ASB specific service. By placing most of the messaging platform details in the +service layer, we are able to handle common things like configuring the connection, binding or creating a specific +queue, etc. in one place. The `IRabbitMqService` and `IAzureServiceBusService` implement the `IEventPublisher` +interface and therefore can also handle directly all the message publishing functionality. + +## Integrations and integration configurations + +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). + +### `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`. + +### `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" }` +- `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 + the provided `EventMessage`. + - The template does not enforce any structure — it could be a freeform text message to send via Slack, or a + JSON body to send via webhook; it is simply stored and used as a string for the most flexibility. + +### `OrganizationIntegrationConfigurationDetails` + +- 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. +- An array of `OrganizationIntegrationConfigurationDetails` is what the `EventIntegrationHandler` fetches from + the database to determine what to publish at the integration level. + +# 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". + +## IntegrationType + +Add a new type to `IntegrationType` for the new integration. + +## Configuration Models + +The configuration models are the classes that will determine what is stored in the database for +`OrganizationIntegration` and `OrganizationIntegrationConfiguration`. The `Configuration` columns are the +serialized version of the corresponding objects and represent the coonfiguration details for this integration +and event type. + +1. `ExampleIntegration` + - Configuration details for the whole integration (e.g. a token in Slack). + - Applies to every event type configuration defined for this integration. + - Maps to the JSON structure stored in `Configuration` in ``OrganizationIntegration`. +2. `ExampleIntegrationConfiguration` + - Configuration details that could change from event to event (e.g. channelId in Slack). + - Maps to the JSON structure stored in `Configuration` in `OrganizationIntegrationConfiguration`. +3. `ExampleIntegrationConfigurationDetails` + - Combined configuration of both Integration _and_ IntegrationConfiguration. + - This will be the deserialized version of the `MergedConfiguration` in + `OrganizationIntegrationConfigurationDetails`. + +## Request Models + +1. Add a new case to the switch method in `OrganizationIntegrationRequestModel.Validate`. +2. Add a new case to the switch method in `OrganizationIntegrationConfigurationRequestModel.IsValidForType`. + +## Integration Handler + +e.g. `ExampleIntegrationHandler` +- This is where the actual code will go to perform the integration (i.e. send an HTTP request, etc.). +- Handlers receive an `IntegrationMessage` where `` is the `ExampleIntegrationConfigurationDetails` + defined above. This has the Configuration as well as the rendered template message to be sent. +- Handlers return an `IntegrationHandlerResult` with details about if the request - success / failure, + if it can be retried, when it should be delayed until, etc. +- The scope of the handler is simply to do the integration and report the result. + Everything else (such as how many times to retry, when to retry, what to do with failures) + is done in the Listener. + +## GlobalSettings + +### RabbitMQ +Add the queue names for the integration. These are typically set with a default value so +that they will be created when first accessed in code by RabbitMQ. + +1. `ExampleEventQueueName` +2. `ExampleIntegrationQueueName` +3. `ExampleIntegrationRetryQueueName` + +### Azure Service Bus +Add the subscription names to use for ASB for this integration. Similar to RabbitMQ a +default value is provided so that we don't require configuring it in secrets but allow +it to be overridden. **However**, unlike RabbitMQ these subscriptions must exist prior +to the code accessing them. They will not be created on the fly. See [Deploying a new +integration](#deploying-a-new-integration) below + +1. `ExmpleEventSubscriptionName` +2. `ExmpleIntegrationSubscriptionName` + +#### Service Bus Emulator, local config +In order to create ASB resources locally, we need to also update the `servicebusemulator_config.json` file +to include any new subscriptions. +- Under the existing event topic (`event-logging`) add a subscription for the event level for this + new integration (`events-example-subscription`). +- Under the existing integration topic (`event-integrations`) add a new subscription for the integration + level messages (`integration-example-subscription`). + - Copy the correlation filter from the other integration level subscriptions. It should filter based on + the `IntegrationType.ToRoutingKey`, or in this example `example`. + +These names added here are what must match the values provided in the secrets or the defaults provided +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. + +## 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. + +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. + +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); +``` + +2. In `AddAzureServiceBusListeners` add the integration: +``` csharp +services.AddAzureServiceBusIntegration( + eventSubscriptionName: globalSettings.EventLogging.AzureServiceBus.ExampleEventSubscriptionName, + integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.ExampleIntegrationSubscriptionName, + integrationType: IntegrationType.Example, + globalSettings: globalSettings); +``` + +# Deploying a new integration + +## RabbitMQ + +RabbitMQ dynamically creates queues and exchanges when they are first accessed in code. +Therefore, there is no need to manually create queues when deploying a new integration. +They can be created and configured ahead of time, but it's not required. Note that once +they are created, if any configurations need to be changed, the queue or exchange must be +deleted and recreated. + +## Azure Service Bus + +Unlike RabbitMQ, ASB resources **must** be allocated before the code accesses them and +will not be created on the fly. This means that any subscriptions needed for a new +integration must be created in ASB before that code is deployed. + +The two subscriptions created above in Global Settings and `servicebusemulator_config.json` +need to be created in the Azure portal or CLI for the environment before deploying the +code. + +1. `ExmpleEventSubscriptionName` + - This subscription is a fan-out subscription from the main event topic. + - As such, it will start receiving all the events as soon as it is declared. + - This can create a backlog before the integration-specific handler is declared and deployed. + - One strategy to avoid this is to create the subscription with a false filter (e.g. `1 = 0`). + - This will create the subscription, but the filter will ensure that no messages + actually land in the subscription. + - Code can be deployed that references the subscription, because the subscription + legitimately exists (it is simply empty). + - When the code is in place, and we're ready to start receiving messages on the new + integration, we simply remove the filter to return the subscription to receiving + all messages via fan-out. +2. `ExmpleIntegrationSubscriptionName` + - This subscription must be created before the new integration code can be deployed. + - However, it is not fan-out, but rather a filter based on the `IntegrationType.ToRoutingKey`. + - Therefore, it won't start receiving messages until organizations have active configurations. + This means there's no risk of building up a backlog by declaring it ahead of time. diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs similarity index 100% rename from src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs rename to src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs similarity index 98% rename from src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs rename to src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs index 5b18d8817c..a60738d62d 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs @@ -2,7 +2,7 @@ using System.Text; using System.Text.Json; -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using RabbitMQ.Client; diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqService.cs similarity index 99% rename from src/Core/AdminConsole/Services/Implementations/RabbitMqService.cs rename to src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqService.cs index 617d1b41fb..20ae31a113 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqService.cs @@ -1,7 +1,7 @@ #nullable enable using System.Text; -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Enums; using Bit.Core.Settings; using RabbitMQ.Client; diff --git a/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs similarity index 91% rename from src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs rename to src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs index fe0f6eabe1..f32d1166fa 100644 --- a/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs @@ -1,6 +1,6 @@ #nullable enable -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/SlackService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs similarity index 100% rename from src/Core/AdminConsole/Services/Implementations/SlackService.cs rename to src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs diff --git a/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs similarity index 97% rename from src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs rename to src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs index df364b2a48..3d76077483 100644 --- a/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs @@ -3,7 +3,7 @@ using System.Globalization; using System.Net; using System.Text; -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; #nullable enable diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 5451400803..f8f8381cc0 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -5,7 +5,7 @@ using System.Security.Cryptography.X509Certificates; using AspNetCoreRateLimit; using Azure.Storage.Queues; using Bit.Core.AdminConsole.Models.Business.Tokenables; -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services.Implementations; diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs index f7863401b5..c72da2b16f 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs @@ -3,7 +3,7 @@ using Bit.Api.AdminConsole.Controllers; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs index 77ce06f4f8..19ae20804b 100644 --- a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs +++ b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs @@ -1,6 +1,6 @@ using System.Text.Json; using Bit.Api.AdminConsole.Models.Request.Organizations; -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Enums; using Xunit; diff --git a/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs similarity index 95% rename from test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs rename to test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs index 6ed84717de..ebb6fa079e 100644 --- a/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs +++ b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs @@ -1,9 +1,9 @@ using System.Text.Json; -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Enums; using Xunit; -namespace Bit.Core.Test.Models.Data.Integrations; +namespace Bit.Core.Test.Models.Data.EventIntegrations; public class IntegrationMessageTests { @@ -45,6 +45,7 @@ public class IntegrationMessageTests var json = message.ToJson(); var result = IntegrationMessage.FromJson(json); + Assert.NotNull(result); Assert.Equal(message.Configuration, result.Configuration); Assert.Equal(message.MessageId, result.MessageId); Assert.Equal(message.RenderedTemplate, result.RenderedTemplate); diff --git a/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs index b1eb117cf0..f53df626d1 100644 --- a/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs @@ -1,7 +1,7 @@ #nullable enable using Azure.Messaging.ServiceBus; -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs index 0962df52cd..d4ceabae75 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs @@ -1,6 +1,6 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; diff --git a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs index 10e42c92cc..7f113cfe53 100644 --- a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Enums; using Bit.Core.Services; using Xunit; diff --git a/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs index 92a51e1831..da0b8ec377 100644 --- a/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs @@ -1,5 +1,5 @@ using System.Text; -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs index 9f66e2eb2f..dab6c41b61 100644 --- a/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs index 7870f543d1..3461b1b607 100644 --- a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs @@ -1,5 +1,5 @@ using System.Net; -using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; From 17507446a46ad6165301e53bbfb5d50d96cc797b Mon Sep 17 00:00:00 2001 From: Alexey Zilber <110793805+alex8bitw@users.noreply.github.com> Date: Tue, 17 Jun 2025 21:35:38 +0800 Subject: [PATCH 06/47] Added SendGridApiHost to Globals (#5961) * Added SendGridApiHost to Globals * Added SendGridApiHost for test coverage --- .../Services/Implementations/SendGridMailDeliveryService.cs | 2 +- src/Core/Settings/GlobalSettings.cs | 1 + test/Core.Test/Services/SendGridMailDeliveryServiceTests.cs | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Core/Services/Implementations/SendGridMailDeliveryService.cs b/src/Core/Services/Implementations/SendGridMailDeliveryService.cs index ea915b56f2..773f87931d 100644 --- a/src/Core/Services/Implementations/SendGridMailDeliveryService.cs +++ b/src/Core/Services/Implementations/SendGridMailDeliveryService.cs @@ -21,7 +21,7 @@ public class SendGridMailDeliveryService : IMailDeliveryService, IDisposable GlobalSettings globalSettings, IWebHostEnvironment hostingEnvironment, ILogger logger) - : this(new SendGridClient(globalSettings.Mail.SendGridApiKey), + : this(new SendGridClient(globalSettings.Mail.SendGridApiKey, globalSettings.Mail.SendGridApiHost), globalSettings, hostingEnvironment, logger) { } diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index f08d66c28f..7a794ec3f6 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -431,6 +431,7 @@ public class GlobalSettings : IGlobalSettings public SmtpSettings Smtp { get; set; } = new SmtpSettings(); public string SendGridApiKey { get; set; } public int? SendGridPercentage { get; set; } + public string SendGridApiHost { get; set; } = "https://api.sendgrid.com"; public class SmtpSettings { diff --git a/test/Core.Test/Services/SendGridMailDeliveryServiceTests.cs b/test/Core.Test/Services/SendGridMailDeliveryServiceTests.cs index 3c64e5c406..a6132543b7 100644 --- a/test/Core.Test/Services/SendGridMailDeliveryServiceTests.cs +++ b/test/Core.Test/Services/SendGridMailDeliveryServiceTests.cs @@ -25,7 +25,8 @@ public class SendGridMailDeliveryServiceTests : IDisposable { Mail = { - SendGridApiKey = "SendGridApiKey" + SendGridApiKey = "SendGridApiKey", + SendGridApiHost = "https://api.sendgrid.com" } }; From 0a5dc04d9e641f8486d317e4d36c806646bfb4d1 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Tue, 17 Jun 2025 10:48:11 -0400 Subject: [PATCH 07/47] [PM-22458] Add user managed privileged apps FF for Android (#5935) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 09c4294cd6..cd2874f792 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -182,6 +182,7 @@ public static class FeatureFlagKeys public const string EnablePMFlightRecorder = "enable-pm-flight-recorder"; public const string MobileErrorReporting = "mobile-error-reporting"; public const string AndroidChromeAutofill = "android-chrome-autofill"; + public const string UserManagedPrivilegedApps = "pm-18970-user-managed-privileged-apps"; public const string EnablePMPreloginSettings = "enable-pm-prelogin-settings"; public const string AppIntents = "app-intents"; From a3c5741164f62c6ee9dd2b14637c900210d78f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Tue, 17 Jun 2025 11:07:26 -0400 Subject: [PATCH 08/47] [PM-22610] validate file within max length; log deletion of invalid uploads (#5960) --- .../Commands/NonAnonymousSendCommand.cs | 47 +++++++++++---- .../Services/AzureSendFileStorageService.cs | 13 ++-- .../Interfaces/ISendStorageService.cs | 19 +++--- .../Services/LocalSendStorageService.cs | 12 ++-- .../NoopSendFileStorageService.cs | 4 +- .../Services/NonAnonymousSendCommandTests.cs | 60 ++++++++++++++----- 6 files changed, 99 insertions(+), 56 deletions(-) diff --git a/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs b/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs index 87b4e581ca..804200a05f 100644 --- a/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs +++ b/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs @@ -8,6 +8,7 @@ using Bit.Core.Tools.Repositories; using Bit.Core.Tools.SendFeatures.Commands.Interfaces; using Bit.Core.Tools.Services; using Bit.Core.Utilities; +using Microsoft.Extensions.Logging; namespace Bit.Core.Tools.SendFeatures.Commands; @@ -18,19 +19,22 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand private readonly IPushNotificationService _pushNotificationService; private readonly ISendValidationService _sendValidationService; private readonly ISendCoreHelperService _sendCoreHelperService; + private readonly ILogger _logger; public NonAnonymousSendCommand(ISendRepository sendRepository, ISendFileStorageService sendFileStorageService, IPushNotificationService pushNotificationService, ISendAuthorizationService sendAuthorizationService, ISendValidationService sendValidationService, - ISendCoreHelperService sendCoreHelperService) + ISendCoreHelperService sendCoreHelperService, + ILogger logger) { _sendRepository = sendRepository; _sendFileStorageService = sendFileStorageService; _pushNotificationService = pushNotificationService; _sendValidationService = sendValidationService; _sendCoreHelperService = sendCoreHelperService; + _logger = logger; } public async Task SaveSendAsync(Send send) @@ -63,6 +67,11 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand throw new BadRequestException("No file data."); } + if (fileLength > SendFileSettingHelper.MAX_FILE_SIZE) + { + throw new BadRequestException($"Max file size is {SendFileSettingHelper.MAX_FILE_SIZE_READABLE}."); + } + var storageBytesRemaining = await _sendValidationService.StorageRemainingForSendAsync(send); if (storageBytesRemaining < fileLength) @@ -77,13 +86,17 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand data.Id = fileId; data.Size = fileLength; data.Validated = false; - send.Data = JsonSerializer.Serialize(data, - JsonHelpers.IgnoreWritingNull); + send.Data = JsonSerializer.Serialize(data, JsonHelpers.IgnoreWritingNull); await SaveSendAsync(send); return await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId); } catch { + _logger.LogWarning( + "Deleted file from {SendId} because an error occurred when creating the upload URL.", + send.Id + ); + // Clean up since this is not transactional await _sendFileStorageService.DeleteFileAsync(send, fileId); throw; @@ -135,23 +148,31 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand { var fileData = JsonSerializer.Deserialize(send.Data); - var (valid, realSize) = await _sendFileStorageService.ValidateFileAsync(send, fileData.Id, fileData.Size, SendFileSettingHelper.FILE_SIZE_LEEWAY); + var minimum = fileData.Size - SendFileSettingHelper.FILE_SIZE_LEEWAY; + var maximum = Math.Min( + fileData.Size + SendFileSettingHelper.FILE_SIZE_LEEWAY, + SendFileSettingHelper.MAX_FILE_SIZE + ); + var (valid, size) = await _sendFileStorageService.ValidateFileAsync(send, fileData.Id, minimum, maximum); - if (!valid || realSize > SendFileSettingHelper.FILE_SIZE_LEEWAY) + // protect file service from upload hijacking by deleting invalid sends + if (!valid) { - // File reported differs in size from that promised. Must be a rogue client. Delete Send + _logger.LogWarning( + "Deleted {SendId} because its reported size {Size} was outside the expected range ({Minimum} - {Maximum}).", + send.Id, + size, + minimum, + maximum + ); await DeleteSendAsync(send); return false; } - // Update Send data if necessary - if (realSize != fileData.Size) - { - fileData.Size = realSize.Value; - } + // replace expected size with validated size + fileData.Size = size; fileData.Validated = true; - send.Data = JsonSerializer.Serialize(fileData, - JsonHelpers.IgnoreWritingNull); + send.Data = JsonSerializer.Serialize(fileData, JsonHelpers.IgnoreWritingNull); await SaveSendAsync(send); return valid; diff --git a/src/Core/Tools/SendFeatures/Services/AzureSendFileStorageService.cs b/src/Core/Tools/SendFeatures/Services/AzureSendFileStorageService.cs index 09f3be29e7..ee54ffd6b6 100644 --- a/src/Core/Tools/SendFeatures/Services/AzureSendFileStorageService.cs +++ b/src/Core/Tools/SendFeatures/Services/AzureSendFileStorageService.cs @@ -88,7 +88,7 @@ public class AzureSendFileStorageService : ISendFileStorageService return sasUri.ToString(); } - public async Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway) + public async Task<(bool, long)> ValidateFileAsync(Send send, string fileId, long minimum, long maximum) { await InitAsync(); @@ -116,17 +116,14 @@ public class AzureSendFileStorageService : ISendFileStorageService await blobClient.SetHttpHeadersAsync(headers); var length = blobProperties.Value.ContentLength; - if (length < expectedFileSize - leeway || length > expectedFileSize + leeway) - { - return (false, length); - } + var valid = minimum <= length || length <= maximum; - return (true, length); + return (valid, length); } catch (Exception ex) { - _logger.LogError(ex, "Unhandled error in ValidateFileAsync"); - return (false, null); + _logger.LogError(ex, $"A storage operation failed in {nameof(ValidateFileAsync)}"); + return (false, -1); } } diff --git a/src/Core/Tools/SendFeatures/Services/Interfaces/ISendStorageService.cs b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendStorageService.cs index 29bc0c6a6a..8712d07d48 100644 --- a/src/Core/Tools/SendFeatures/Services/Interfaces/ISendStorageService.cs +++ b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendStorageService.cs @@ -56,16 +56,13 @@ public interface ISendFileStorageService /// /// used to help validate file /// File id to identify which file to validate - /// Expected file size of the file - /// - /// Send file size tolerance in bytes. When an uploaded file's `expectedFileSize` - /// is outside of the leeway, the storage operation fails. - /// - /// - /// ❌ Fill this in with an explanation of the error thrown when `leeway` is incorrect - /// - /// Task object for async operations with Tuple of boolean that determines if file was valid and long that - /// the actual file size of the file. + /// The minimum allowed length of the stored file in bytes. + /// The maximuim allowed length of the stored file in bytes + /// + /// A task that completes when validation is finished. The first element of the tuple is + /// when validation succeeded, and false otherwise. The second element + /// of the tuple contains the observed file length in bytes. If an error occurs during validation, + /// this returns `-1`. /// - Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway); + Task<(bool valid, long length)> ValidateFileAsync(Send send, string fileId, long minimum, long maximum); } diff --git a/src/Core/Tools/SendFeatures/Services/LocalSendStorageService.cs b/src/Core/Tools/SendFeatures/Services/LocalSendStorageService.cs index c205028d9e..a6b3fb0faf 100644 --- a/src/Core/Tools/SendFeatures/Services/LocalSendStorageService.cs +++ b/src/Core/Tools/SendFeatures/Services/LocalSendStorageService.cs @@ -85,9 +85,9 @@ public class LocalSendStorageService : ISendFileStorageService public Task GetSendFileUploadUrlAsync(Send send, string fileId) => Task.FromResult($"/sends/{send.Id}/file/{fileId}"); - public Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway) + public Task<(bool, long)> ValidateFileAsync(Send send, string fileId, long minimum, long maximum) { - long? length = null; + long length = -1; var path = FilePath(send, fileId); if (!File.Exists(path)) { @@ -95,11 +95,7 @@ public class LocalSendStorageService : ISendFileStorageService } length = new FileInfo(path).Length; - if (expectedFileSize < length - leeway || expectedFileSize > length + leeway) - { - return Task.FromResult((false, length)); - } - - return Task.FromResult((true, length)); + var valid = minimum < length || length < maximum; + return Task.FromResult((valid, length)); } } diff --git a/src/Core/Tools/Services/NoopImplementations/NoopSendFileStorageService.cs b/src/Core/Tools/Services/NoopImplementations/NoopSendFileStorageService.cs index 4fb841e7a3..16c20e521e 100644 --- a/src/Core/Tools/Services/NoopImplementations/NoopSendFileStorageService.cs +++ b/src/Core/Tools/Services/NoopImplementations/NoopSendFileStorageService.cs @@ -37,8 +37,8 @@ public class NoopSendFileStorageService : ISendFileStorageService return Task.FromResult((string)null); } - public Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway) + public Task<(bool, long)> ValidateFileAsync(Send send, string fileId, long minimum, long maximum) { - return Task.FromResult((false, default(long?))); + return Task.FromResult((false, -1L)); } } diff --git a/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs b/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs index 674cca7d5f..1ad6a08516 100644 --- a/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs +++ b/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs @@ -10,10 +10,10 @@ using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; -using Bit.Core.Tools.SendFeatures; using Bit.Core.Tools.SendFeatures.Commands; using Bit.Core.Tools.Services; using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Logging; using NSubstitute; using NSubstitute.ExceptionExtensions; using Xunit; @@ -35,6 +35,8 @@ public class NonAnonymousSendCommandTests private readonly ISendCoreHelperService _sendCoreHelperService; private readonly NonAnonymousSendCommand _nonAnonymousSendCommand; + private readonly ILogger _logger; + public NonAnonymousSendCommandTests() { _sendRepository = Substitute.For(); @@ -45,6 +47,7 @@ public class NonAnonymousSendCommandTests _sendValidationService = Substitute.For(); _currentContext = Substitute.For(); _sendCoreHelperService = Substitute.For(); + _logger = Substitute.For>(); _nonAnonymousSendCommand = new NonAnonymousSendCommand( _sendRepository, @@ -52,7 +55,8 @@ public class NonAnonymousSendCommandTests _pushNotificationService, _sendAuthorizationService, _sendValidationService, - _sendCoreHelperService + _sendCoreHelperService, + _logger ); } @@ -652,11 +656,11 @@ public class NonAnonymousSendCommandTests UserId = userId }; var fileData = new SendFileData(); - var fileLength = 15L * 1024L * 1024L * 1024L; // 15GB + var fileLength = 15L * 1024L * 1024L; // 15 MB - // Configure validation service to return large but insufficient storage (10GB for self-hosted non-premium) + // Configure validation service to return insufficient storage _sendValidationService.StorageRemainingForSendAsync(send) - .Returns(10L * 1024L * 1024L * 1024L); // 10GB remaining (self-hosted default) + .Returns(10L * 1024L * 1024L); // 10 MB remaining // Act & Assert var exception = await Assert.ThrowsAsync(() => @@ -687,11 +691,40 @@ public class NonAnonymousSendCommandTests UserId = userId }; var fileData = new SendFileData(); - var fileLength = 2L * 1024L * 1024L * 1024L; // 2GB + var fileLength = 2L * 1024L * 1024L * 1024L; // 2MB - // Configure validation service to return 1GB storage (cloud non-premium default) + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("Max file size is ", exception.Message); + + // Verify no further methods were called + await _sendValidationService.DidNotReceive().StorageRemainingForSendAsync(Arg.Any()); + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_UserCanAccessPremium_IsNotPremium_IsNotSelfHosted_NotEnoughSpace_ThrowsBadRequest() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 2L * 1024L * 1024L; // 2MB + + // Configure validation service to return 1 MB storage remaining _sendValidationService.StorageRemainingForSendAsync(send) - .Returns(1L * 1024L * 1024L * 1024L); // 1GB remaining (cloud default) + .Returns(1L * 1024L * 1024L); // Act & Assert var exception = await Assert.ThrowsAsync(() => @@ -756,7 +789,7 @@ public class NonAnonymousSendCommandTests UserId = null }; var fileData = new SendFileData(); - var fileLength = 2L * 1024L * 1024L * 1024L; // 2GB + var fileLength = 2L * 1024L * 1024L; // 2 MB // Configure validation service to throw BadRequest when checking storage for org without storage _sendValidationService.StorageRemainingForSendAsync(send) @@ -792,11 +825,10 @@ public class NonAnonymousSendCommandTests UserId = null }; var fileData = new SendFileData(); - var fileLength = 2L * 1024L * 1024L * 1024L; // 2GB + var fileLength = 2L * 1024L * 1024L; // 2 MB - // Configure validation service to return 1GB storage (org's max storage limit) _sendValidationService.StorageRemainingForSendAsync(send) - .Returns(1L * 1024L * 1024L * 1024L); // 1GB remaining + .Returns(1L * 1024L * 1024L); // 1 MB remaining // Act & Assert var exception = await Assert.ThrowsAsync(() => @@ -980,7 +1012,7 @@ public class NonAnonymousSendCommandTests }; // Setup validation to succeed - _sendFileStorageService.ValidateFileAsync(send, sendFileData.Id, sendFileData.Size, SendFileSettingHelper.FILE_SIZE_LEEWAY).Returns((true, sendFileData.Size)); + _sendFileStorageService.ValidateFileAsync(send, sendFileData.Id, Arg.Any(), Arg.Any()).Returns((true, sendFileData.Size)); // Act await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send); @@ -1014,7 +1046,7 @@ public class NonAnonymousSendCommandTests Data = JsonSerializer.Serialize(sendFileData) }; - _sendFileStorageService.ValidateFileAsync(send, sendFileData.Id, sendFileData.Size, SendFileSettingHelper.FILE_SIZE_LEEWAY).Returns((true, sendFileData.Size)); + _sendFileStorageService.ValidateFileAsync(send, sendFileData.Id, Arg.Any(), Arg.Any()).Returns((true, sendFileData.Size)); // Act await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send); From 6dc26f4be6b220d0741559cd2dd9da78f7aebfef Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Tue, 17 Jun 2025 10:55:42 -0500 Subject: [PATCH 09/47] chore: remove external id feature flag, refs PM-18634 (#5974) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index cd2874f792..40a0b8a1b0 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -107,7 +107,6 @@ public static class FeatureFlagKeys 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"; - public const string SsoExternalIdVisibility = "pm-18630-sso-external-id-visibility"; 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"; From 6800bc57f3eb492222e128cffcd00e16b29cc155 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 17 Jun 2025 13:30:56 -0400 Subject: [PATCH 10/47] [PM-18555] Main part of notifications refactor (#5757) * More tests * More tests * Add non-guid tests * Introduce slimmer services * Implement IPushEngine on services * Implement IPushEngine * Fix tests * Format * Switch to `Guid` on `PushSendRequestModel` * Remove TODOs --- .../Push/Controllers/PushController.cs | 54 +- .../Api/Request/PushSendRequestModel.cs | 18 +- .../NotificationHubPushNotificationService.cs | 408 +++------------ .../AzureQueuePushNotificationService.cs | 226 +------- .../Platform/Push/Services/IPushEngine.cs | 13 + .../Push/Services/IPushNotificationService.cs | 425 ++++++++++++++- .../Platform/Push/Services/IPushRelayer.cs | 44 ++ .../MultiServicePushNotificationService.cs | 215 ++------ .../Services/NoopPushNotificationService.cs | 123 +---- ...NotificationsApiPushNotificationService.cs | 237 +-------- .../Push/Services/PushNotification.cs | 78 +++ .../Services/RelayPushNotificationService.cs | 353 ++----------- .../Utilities/ServiceCollectionExtensions.cs | 11 +- .../Factories/ApiApplicationFactory.cs | 2 + .../Controllers/PushControllerTests.cs | 449 ++++++++++++++++ .../Push/Controllers/PushControllerTests.cs | 204 -------- .../Api/Request/PushSendRequestModelTests.cs | 74 ++- ...ficationHubPushNotificationServiceTests.cs | 487 +----------------- .../AzureQueuePushNotificationServiceTests.cs | 99 +--- ...ultiServicePushNotificationServiceTests.cs | 92 +--- ...icationsApiPushNotificationServiceTests.cs | 11 +- .../Platform/Push/Services/PushTestBase.cs | 21 +- .../RelayPushNotificationServiceTests.cs | 35 +- 23 files changed, 1271 insertions(+), 2408 deletions(-) create mode 100644 src/Core/Platform/Push/Services/IPushEngine.cs create mode 100644 src/Core/Platform/Push/Services/IPushRelayer.cs create mode 100644 src/Core/Platform/Push/Services/PushNotification.cs create mode 100644 test/Api.IntegrationTest/Platform/Controllers/PushControllerTests.cs diff --git a/src/Api/Platform/Push/Controllers/PushController.cs b/src/Api/Platform/Push/Controllers/PushController.cs index 2a1f2b987d..af24a7b2ca 100644 --- a/src/Api/Platform/Push/Controllers/PushController.cs +++ b/src/Api/Platform/Push/Controllers/PushController.cs @@ -1,8 +1,11 @@ -using Bit.Core.Context; +using System.Diagnostics; +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.Settings; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; @@ -20,14 +23,14 @@ namespace Bit.Api.Platform.Push; public class PushController : Controller { private readonly IPushRegistrationService _pushRegistrationService; - private readonly IPushNotificationService _pushNotificationService; + private readonly IPushRelayer _pushRelayer; private readonly IWebHostEnvironment _environment; private readonly ICurrentContext _currentContext; private readonly IGlobalSettings _globalSettings; public PushController( IPushRegistrationService pushRegistrationService, - IPushNotificationService pushNotificationService, + IPushRelayer pushRelayer, IWebHostEnvironment environment, ICurrentContext currentContext, IGlobalSettings globalSettings) @@ -35,7 +38,7 @@ public class PushController : Controller _currentContext = currentContext; _environment = environment; _pushRegistrationService = pushRegistrationService; - _pushNotificationService = pushNotificationService; + _pushRelayer = pushRelayer; _globalSettings = globalSettings; } @@ -74,31 +77,50 @@ public class PushController : Controller } [HttpPost("send")] - public async Task SendAsync([FromBody] PushSendRequestModel model) + public async Task SendAsync([FromBody] PushSendRequestModel model) { CheckUsage(); - if (!string.IsNullOrWhiteSpace(model.InstallationId)) + NotificationTarget target; + Guid targetId; + + if (model.InstallationId.HasValue) { - if (_currentContext.InstallationId!.Value.ToString() != model.InstallationId!) + if (_currentContext.InstallationId!.Value != model.InstallationId.Value) { throw new BadRequestException("InstallationId does not match current context."); } - await _pushNotificationService.SendPayloadToInstallationAsync( - _currentContext.InstallationId.Value.ToString(), model.Type, model.Payload, Prefix(model.Identifier), - Prefix(model.DeviceId), model.ClientType); + target = NotificationTarget.Installation; + targetId = _currentContext.InstallationId.Value; } - else if (!string.IsNullOrWhiteSpace(model.UserId)) + else if (model.UserId.HasValue) { - await _pushNotificationService.SendPayloadToUserAsync(Prefix(model.UserId), - model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType); + target = NotificationTarget.User; + targetId = model.UserId.Value; } - else if (!string.IsNullOrWhiteSpace(model.OrganizationId)) + else if (model.OrganizationId.HasValue) { - await _pushNotificationService.SendPayloadToOrganizationAsync(Prefix(model.OrganizationId), - model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType); + target = NotificationTarget.Organization; + targetId = model.OrganizationId.Value; } + else + { + throw new UnreachableException("Model validation should have prevented getting here."); + } + + var notification = new RelayedNotification + { + Type = model.Type, + Target = target, + TargetId = targetId, + Payload = model.Payload, + Identifier = model.Identifier, + DeviceId = model.DeviceId, + ClientType = model.ClientType, + }; + + await _pushRelayer.RelayAsync(_currentContext.InstallationId.Value, notification); } private string Prefix(string value) diff --git a/src/Core/Models/Api/Request/PushSendRequestModel.cs b/src/Core/Models/Api/Request/PushSendRequestModel.cs index 0ef7e999e3..19f89d931f 100644 --- a/src/Core/Models/Api/Request/PushSendRequestModel.cs +++ b/src/Core/Models/Api/Request/PushSendRequestModel.cs @@ -4,22 +4,22 @@ using Bit.Core.Enums; namespace Bit.Core.Models.Api; -public class PushSendRequestModel : IValidatableObject +public class PushSendRequestModel : IValidatableObject { - public string? UserId { get; set; } - public string? OrganizationId { get; set; } - public string? DeviceId { get; set; } + public Guid? UserId { get; set; } + public Guid? OrganizationId { get; set; } + public Guid? DeviceId { get; set; } public string? Identifier { get; set; } public required PushType Type { get; set; } - public required object Payload { get; set; } + public required T Payload { get; set; } public ClientType? ClientType { get; set; } - public string? InstallationId { get; set; } + public Guid? InstallationId { get; set; } public IEnumerable Validate(ValidationContext validationContext) { - if (string.IsNullOrWhiteSpace(UserId) && - string.IsNullOrWhiteSpace(OrganizationId) && - string.IsNullOrWhiteSpace(InstallationId)) + if (!UserId.HasValue && + !OrganizationId.HasValue && + !InstallationId.HasValue) { yield return new ValidationResult( $"{nameof(UserId)} or {nameof(OrganizationId)} or {nameof(InstallationId)} is required."); diff --git a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs index 368c0f731b..81ec82a25d 100644 --- a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs @@ -1,21 +1,17 @@ #nullable enable using System.Text.Json; using System.Text.RegularExpressions; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Auth.Entities; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models; using Bit.Core.Models.Data; -using Bit.Core.NotificationCenter.Entities; using Bit.Core.Platform.Push; +using Bit.Core.Platform.Push.Internal; using Bit.Core.Repositories; using Bit.Core.Settings; -using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using Notification = Bit.Core.NotificationCenter.Entities.Notification; namespace Bit.Core.NotificationHub; @@ -26,52 +22,32 @@ namespace Bit.Core.NotificationHub; /// Used by Cloud-Hosted environments. /// Received by Firebase for Android or APNS for iOS. /// -public class NotificationHubPushNotificationService : IPushNotificationService +public class NotificationHubPushNotificationService : IPushEngine, IPushRelayer { private readonly IInstallationDeviceRepository _installationDeviceRepository; private readonly IHttpContextAccessor _httpContextAccessor; private readonly bool _enableTracing = false; private readonly INotificationHubPool _notificationHubPool; private readonly ILogger _logger; - private readonly IGlobalSettings _globalSettings; - private readonly TimeProvider _timeProvider; public NotificationHubPushNotificationService( IInstallationDeviceRepository installationDeviceRepository, INotificationHubPool notificationHubPool, IHttpContextAccessor httpContextAccessor, ILogger logger, - IGlobalSettings globalSettings, - TimeProvider timeProvider) + IGlobalSettings globalSettings) { _installationDeviceRepository = installationDeviceRepository; _httpContextAccessor = httpContextAccessor; _notificationHubPool = notificationHubPool; _logger = logger; - _globalSettings = globalSettings; - _timeProvider = timeProvider; if (globalSettings.Installation.Id == Guid.Empty) { logger.LogWarning("Installation ID is not set. Push notifications for installations will not work."); } } - public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable collectionIds) - { - await PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds); - } - - public async Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable collectionIds) - { - await PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds); - } - - public async Task PushSyncCipherDeleteAsync(Cipher cipher) - { - await PushCipherAsync(cipher, PushType.SyncLoginDelete, null); - } - - private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable? collectionIds) + public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable? collectionIds) { if (cipher.OrganizationId.HasValue) { @@ -93,311 +69,17 @@ public class NotificationHubPushNotificationService : IPushNotificationService CollectionIds = collectionIds, }; - await SendPayloadToUserAsync(cipher.UserId.Value, type, message, true); - } - } - - public async Task PushSyncFolderCreateAsync(Folder folder) - { - await PushFolderAsync(folder, PushType.SyncFolderCreate); - } - - public async Task PushSyncFolderUpdateAsync(Folder folder) - { - await PushFolderAsync(folder, PushType.SyncFolderUpdate); - } - - public async Task PushSyncFolderDeleteAsync(Folder folder) - { - await PushFolderAsync(folder, PushType.SyncFolderDelete); - } - - private async Task PushFolderAsync(Folder folder, PushType type) - { - var message = new SyncFolderPushNotification - { - Id = folder.Id, - UserId = folder.UserId, - RevisionDate = folder.RevisionDate - }; - - await SendPayloadToUserAsync(folder.UserId, type, message, true); - } - - public async Task PushSyncCiphersAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncCiphers); - } - - public async Task PushSyncVaultAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncVault); - } - - public async Task PushSyncOrganizationsAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncOrganizations); - } - - public async Task PushSyncOrgKeysAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncOrgKeys); - } - - public async Task PushSyncSettingsAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncSettings); - } - - public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false) - { - await PushUserAsync(userId, PushType.LogOut, excludeCurrentContext); - } - - private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false) - { - var message = new UserPushNotification { UserId = userId, Date = _timeProvider.GetUtcNow().UtcDateTime }; - - await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext); - } - - public async Task PushSyncSendCreateAsync(Send send) - { - await PushSendAsync(send, PushType.SyncSendCreate); - } - - public async Task PushSyncSendUpdateAsync(Send send) - { - await PushSendAsync(send, PushType.SyncSendUpdate); - } - - public async Task PushSyncSendDeleteAsync(Send send) - { - await PushSendAsync(send, PushType.SyncSendDelete); - } - - private async Task PushSendAsync(Send send, PushType type) - { - if (send.UserId.HasValue) - { - var message = new SyncSendPushNotification + await PushAsync(new PushNotification { - Id = send.Id, - UserId = send.UserId.Value, - RevisionDate = send.RevisionDate - }; - - await SendPayloadToUserAsync(message.UserId, type, message, true); + Type = type, + Target = NotificationTarget.User, + TargetId = cipher.UserId.Value, + Payload = message, + ExcludeCurrentContext = true, + }); } } - public async Task PushAuthRequestAsync(AuthRequest authRequest) - { - await PushAuthRequestAsync(authRequest, PushType.AuthRequest); - } - - public async Task PushAuthRequestResponseAsync(AuthRequest authRequest) - { - await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse); - } - - public async Task PushNotificationAsync(Notification notification) - { - Guid? installationId = notification.Global && _globalSettings.Installation.Id != Guid.Empty - ? _globalSettings.Installation.Id - : null; - - var message = new NotificationPushNotification - { - Id = notification.Id, - Priority = notification.Priority, - Global = notification.Global, - ClientType = notification.ClientType, - UserId = notification.UserId, - OrganizationId = notification.OrganizationId, - InstallationId = installationId, - TaskId = notification.TaskId, - Title = notification.Title, - Body = notification.Body, - CreationDate = notification.CreationDate, - RevisionDate = notification.RevisionDate - }; - - if (notification.Global) - { - if (installationId.HasValue) - { - await SendPayloadToInstallationAsync(installationId.Value, PushType.Notification, message, true, - notification.ClientType); - } - else - { - _logger.LogWarning( - "Invalid global notification id {NotificationId} push notification. No installation id provided.", - notification.Id); - } - } - else if (notification.UserId.HasValue) - { - await SendPayloadToUserAsync(notification.UserId.Value, PushType.Notification, message, true, - notification.ClientType); - } - else if (notification.OrganizationId.HasValue) - { - await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.Notification, message, - true, notification.ClientType); - } - else - { - _logger.LogWarning("Invalid notification id {NotificationId} push notification", notification.Id); - } - } - - public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) - { - Guid? installationId = notification.Global && _globalSettings.Installation.Id != Guid.Empty - ? _globalSettings.Installation.Id - : null; - - var message = new NotificationPushNotification - { - Id = notification.Id, - Priority = notification.Priority, - Global = notification.Global, - ClientType = notification.ClientType, - UserId = notification.UserId, - OrganizationId = notification.OrganizationId, - InstallationId = installationId, - TaskId = notification.TaskId, - Title = notification.Title, - Body = notification.Body, - CreationDate = notification.CreationDate, - RevisionDate = notification.RevisionDate, - ReadDate = notificationStatus.ReadDate, - DeletedDate = notificationStatus.DeletedDate - }; - - if (notification.Global) - { - if (installationId.HasValue) - { - await SendPayloadToInstallationAsync(installationId.Value, PushType.NotificationStatus, message, true, - notification.ClientType); - } - else - { - _logger.LogWarning( - "Invalid global notification status id {NotificationId} push notification. No installation id provided.", - notification.Id); - } - } - else if (notification.UserId.HasValue) - { - await SendPayloadToUserAsync(notification.UserId.Value, PushType.NotificationStatus, message, true, - notification.ClientType); - } - else if (notification.OrganizationId.HasValue) - { - await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.NotificationStatus, - message, true, notification.ClientType); - } - else - { - _logger.LogWarning("Invalid notification status id {NotificationId} push notification", notification.Id); - } - } - - private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type) - { - var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId }; - - await SendPayloadToUserAsync(authRequest.UserId, type, message, true); - } - - private async Task SendPayloadToInstallationAsync(Guid installationId, PushType type, object payload, - bool excludeCurrentContext, ClientType? clientType = null) - { - await SendPayloadToInstallationAsync(installationId.ToString(), type, payload, - GetContextIdentifier(excludeCurrentContext), clientType: clientType); - } - - private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext, - ClientType? clientType = null) - { - await SendPayloadToUserAsync(userId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext), - clientType: clientType); - } - - private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload, - bool excludeCurrentContext, ClientType? clientType = null) - { - await SendPayloadToOrganizationAsync(orgId.ToString(), type, payload, - GetContextIdentifier(excludeCurrentContext), clientType: clientType); - } - - public async Task PushPendingSecurityTasksAsync(Guid userId) - { - await PushUserAsync(userId, PushType.PendingSecurityTasks); - } - - public async Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, - string? identifier, string? deviceId = null, ClientType? clientType = null) - { - var tag = BuildTag($"template:payload && installationId:{installationId}", identifier, clientType); - await SendPayloadAsync(tag, type, payload); - if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) - { - await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId)); - } - } - - public async Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null) - { - var tag = BuildTag($"template:payload_userId:{SanitizeTagInput(userId)}", identifier, clientType); - await SendPayloadAsync(tag, type, payload); - if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) - { - await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId)); - } - } - - public async Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null) - { - var tag = BuildTag($"template:payload && organizationId:{SanitizeTagInput(orgId)}", identifier, clientType); - await SendPayloadAsync(tag, type, payload); - if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) - { - await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId)); - } - } - - public async Task PushSyncOrganizationStatusAsync(Organization organization) - { - var message = new OrganizationStatusPushNotification - { - OrganizationId = organization.Id, - Enabled = organization.Enabled - }; - - await SendPayloadToOrganizationAsync(organization.Id, PushType.SyncOrganizationStatusChanged, message, false); - } - - public async Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) => - await SendPayloadToOrganizationAsync( - organization.Id, - PushType.SyncOrganizationCollectionSettingChanged, - new OrganizationCollectionManagementPushNotification - { - OrganizationId = organization.Id, - LimitCollectionCreation = organization.LimitCollectionCreation, - LimitCollectionDeletion = organization.LimitCollectionDeletion, - LimitItemDeletion = organization.LimitItemDeletion - }, - false - ); - private string? GetContextIdentifier(bool excludeCurrentContext) { if (!excludeCurrentContext) @@ -425,13 +107,73 @@ public class NotificationHubPushNotificationService : IPushNotificationService return $"({tag})"; } - private async Task SendPayloadAsync(string tag, PushType type, object payload) + public async Task PushAsync(PushNotification pushNotification) + where T : class { + var initialTag = pushNotification.Target switch + { + NotificationTarget.User => $"template:payload_userId:{pushNotification.TargetId}", + NotificationTarget.Organization => $"template:payload && organizationId:{pushNotification.TargetId}", + NotificationTarget.Installation => $"template:payload && installationId:{pushNotification.TargetId}", + _ => throw new InvalidOperationException($"Push notification target '{pushNotification.Target}' is not valid."), + }; + + await PushCoreAsync( + initialTag, + GetContextIdentifier(pushNotification.ExcludeCurrentContext), + pushNotification.Type, + pushNotification.ClientType, + pushNotification.Payload + ); + } + + public async Task RelayAsync(Guid fromInstallation, RelayedNotification relayedNotification) + { + // Relayed notifications need identifiers prefixed with the installation they are from and a underscore + var initialTag = relayedNotification.Target switch + { + NotificationTarget.User => $"template:payload_userId:{fromInstallation}_{relayedNotification.TargetId}", + NotificationTarget.Organization => $"template:payload && organizationId:{fromInstallation}_{relayedNotification.TargetId}", + NotificationTarget.Installation => $"template:payload && installationId:{fromInstallation}", + _ => throw new InvalidOperationException($"Invalid Notification target {relayedNotification.Target}"), + }; + + await PushCoreAsync( + initialTag, + relayedNotification.Identifier, + relayedNotification.Type, + relayedNotification.ClientType, + relayedNotification.Payload + ); + + if (relayedNotification.DeviceId.HasValue) + { + await _installationDeviceRepository.UpsertAsync( + new InstallationDeviceEntity(fromInstallation, relayedNotification.DeviceId.Value) + ); + } + else + { + _logger.LogWarning( + "A related notification of type '{Type}' came through without a device id from installation {Installation}", + relayedNotification.Type, + fromInstallation + ); + } + } + + private async Task PushCoreAsync(string initialTag, string? contextId, PushType pushType, ClientType? clientType, T payload) + { + var finalTag = BuildTag(initialTag, contextId, clientType); + var results = await _notificationHubPool.AllClients.SendTemplateNotificationAsync( new Dictionary { - { "type", ((byte)type).ToString() }, { "payload", JsonSerializer.Serialize(payload) } - }, tag); + { "type", ((byte)pushType).ToString() }, + { "payload", JsonSerializer.Serialize(payload) }, + }, + finalTag + ); if (_enableTracing) { @@ -444,7 +186,7 @@ public class NotificationHubPushNotificationService : IPushNotificationService _logger.LogInformation( "Azure Notification Hub Tracking ID: {Id} | {Type} push notification with {Success} successes and {Failure} failures with a payload of {@Payload} and result of {@Results}", - outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results); + outcome.TrackingId, pushType, outcome.Success, outcome.Failure, payload, outcome.Results); } } } diff --git a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs index 05d1dd2d1d..94a20f1971 100644 --- a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs +++ b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs @@ -1,14 +1,10 @@ #nullable enable using System.Text.Json; using Azure.Storage.Queues; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Auth.Entities; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models; -using Bit.Core.NotificationCenter.Entities; using Bit.Core.Settings; -using Bit.Core.Tools.Entities; using Bit.Core.Utilities; using Bit.Core.Vault.Entities; using Microsoft.AspNetCore.Http; @@ -17,12 +13,10 @@ using Microsoft.Extensions.Logging; namespace Bit.Core.Platform.Push.Internal; -public class AzureQueuePushNotificationService : IPushNotificationService +public class AzureQueuePushNotificationService : IPushEngine { private readonly QueueClient _queueClient; private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IGlobalSettings _globalSettings; - private readonly TimeProvider _timeProvider; public AzureQueuePushNotificationService( [FromKeyedServices("notifications")] QueueClient queueClient, @@ -33,30 +27,13 @@ public class AzureQueuePushNotificationService : IPushNotificationService { _queueClient = queueClient; _httpContextAccessor = httpContextAccessor; - _globalSettings = globalSettings; - _timeProvider = timeProvider; if (globalSettings.Installation.Id == Guid.Empty) { logger.LogWarning("Installation ID is not set. Push notifications for installations will not work."); } } - public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable collectionIds) - { - await PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds); - } - - public async Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable collectionIds) - { - await PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds); - } - - public async Task PushSyncCipherDeleteAsync(Cipher cipher) - { - await PushCipherAsync(cipher, PushType.SyncLoginDelete, null); - } - - private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable? collectionIds) + public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable? collectionIds) { if (cipher.OrganizationId.HasValue) { @@ -83,166 +60,6 @@ public class AzureQueuePushNotificationService : IPushNotificationService } } - public async Task PushSyncFolderCreateAsync(Folder folder) - { - await PushFolderAsync(folder, PushType.SyncFolderCreate); - } - - public async Task PushSyncFolderUpdateAsync(Folder folder) - { - await PushFolderAsync(folder, PushType.SyncFolderUpdate); - } - - public async Task PushSyncFolderDeleteAsync(Folder folder) - { - await PushFolderAsync(folder, PushType.SyncFolderDelete); - } - - private async Task PushFolderAsync(Folder folder, PushType type) - { - var message = new SyncFolderPushNotification - { - Id = folder.Id, - UserId = folder.UserId, - RevisionDate = folder.RevisionDate - }; - - await SendMessageAsync(type, message, true); - } - - public async Task PushSyncCiphersAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncCiphers); - } - - public async Task PushSyncVaultAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncVault); - } - - public async Task PushSyncOrganizationsAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncOrganizations); - } - - public async Task PushSyncOrgKeysAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncOrgKeys); - } - - public async Task PushSyncSettingsAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncSettings); - } - - public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false) - { - await PushUserAsync(userId, PushType.LogOut, excludeCurrentContext); - } - - private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false) - { - var message = new UserPushNotification { UserId = userId, Date = _timeProvider.GetUtcNow().UtcDateTime }; - - await SendMessageAsync(type, message, excludeCurrentContext); - } - - public async Task PushAuthRequestAsync(AuthRequest authRequest) - { - await PushAuthRequestAsync(authRequest, PushType.AuthRequest); - } - - public async Task PushAuthRequestResponseAsync(AuthRequest authRequest) - { - await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse); - } - - private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type) - { - var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId }; - - await SendMessageAsync(type, message, true); - } - - public async Task PushSyncSendCreateAsync(Send send) - { - await PushSendAsync(send, PushType.SyncSendCreate); - } - - public async Task PushSyncSendUpdateAsync(Send send) - { - await PushSendAsync(send, PushType.SyncSendUpdate); - } - - public async Task PushSyncSendDeleteAsync(Send send) - { - await PushSendAsync(send, PushType.SyncSendDelete); - } - - public async Task PushNotificationAsync(Notification notification) - { - var message = new NotificationPushNotification - { - Id = notification.Id, - Priority = notification.Priority, - Global = notification.Global, - ClientType = notification.ClientType, - UserId = notification.UserId, - OrganizationId = notification.OrganizationId, - InstallationId = notification.Global ? _globalSettings.Installation.Id : null, - TaskId = notification.TaskId, - Title = notification.Title, - Body = notification.Body, - CreationDate = notification.CreationDate, - RevisionDate = notification.RevisionDate - }; - - await SendMessageAsync(PushType.Notification, message, true); - } - - public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) - { - var message = new NotificationPushNotification - { - Id = notification.Id, - Priority = notification.Priority, - Global = notification.Global, - ClientType = notification.ClientType, - UserId = notification.UserId, - OrganizationId = notification.OrganizationId, - InstallationId = notification.Global ? _globalSettings.Installation.Id : null, - TaskId = notification.TaskId, - Title = notification.Title, - Body = notification.Body, - CreationDate = notification.CreationDate, - RevisionDate = notification.RevisionDate, - ReadDate = notificationStatus.ReadDate, - DeletedDate = notificationStatus.DeletedDate - }; - - await SendMessageAsync(PushType.NotificationStatus, message, true); - } - - public async Task PushPendingSecurityTasksAsync(Guid userId) - { - await PushUserAsync(userId, PushType.PendingSecurityTasks); - } - - private async Task PushSendAsync(Send send, PushType type) - { - if (send.UserId.HasValue) - { - var message = new SyncSendPushNotification - { - Id = send.Id, - UserId = send.UserId.Value, - RevisionDate = send.RevisionDate - }; - - await SendMessageAsync(type, message, true); - } - } - private async Task SendMessageAsync(PushType type, T payload, bool excludeCurrentContext) { var contextId = GetContextIdentifier(excludeCurrentContext); @@ -263,42 +80,9 @@ public class AzureQueuePushNotificationService : IPushNotificationService return currentContext?.DeviceIdentifier; } - public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null) => - // Noop - Task.CompletedTask; - - public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null) + public async Task PushAsync(PushNotification pushNotification) + where T : class { - // Noop - return Task.FromResult(0); + await SendMessageAsync(pushNotification.Type, pushNotification.Payload, pushNotification.ExcludeCurrentContext); } - - public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null) - { - // Noop - return Task.FromResult(0); - } - - public async Task PushSyncOrganizationStatusAsync(Organization organization) - { - var message = new OrganizationStatusPushNotification - { - OrganizationId = organization.Id, - Enabled = organization.Enabled - }; - await SendMessageAsync(PushType.SyncOrganizationStatusChanged, message, false); - } - - public async Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) => - await SendMessageAsync(PushType.SyncOrganizationCollectionSettingChanged, - new OrganizationCollectionManagementPushNotification - { - OrganizationId = organization.Id, - LimitCollectionCreation = organization.LimitCollectionCreation, - LimitCollectionDeletion = organization.LimitCollectionDeletion, - LimitItemDeletion = organization.LimitItemDeletion - }, false); } diff --git a/src/Core/Platform/Push/Services/IPushEngine.cs b/src/Core/Platform/Push/Services/IPushEngine.cs new file mode 100644 index 0000000000..bde4ddaf4b --- /dev/null +++ b/src/Core/Platform/Push/Services/IPushEngine.cs @@ -0,0 +1,13 @@ +#nullable enable +using Bit.Core.Enums; +using Bit.Core.Vault.Entities; + +namespace Bit.Core.Platform.Push; + +public interface IPushEngine +{ + Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable? collectionIds); + + Task PushAsync(PushNotification pushNotification) + where T : class; +} diff --git a/src/Core/Platform/Push/Services/IPushNotificationService.cs b/src/Core/Platform/Push/Services/IPushNotificationService.cs index 60f3c35089..58b8a4722d 100644 --- a/src/Core/Platform/Push/Services/IPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/IPushNotificationService.cs @@ -2,41 +2,410 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Enums; +using Bit.Core.Models; using Bit.Core.NotificationCenter.Entities; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; +using Microsoft.Extensions.Logging; namespace Bit.Core.Platform.Push; public interface IPushNotificationService { - Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable collectionIds); - Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable collectionIds); - Task PushSyncCipherDeleteAsync(Cipher cipher); - Task PushSyncFolderCreateAsync(Folder folder); - Task PushSyncFolderUpdateAsync(Folder folder); - Task PushSyncFolderDeleteAsync(Folder folder); - Task PushSyncCiphersAsync(Guid userId); - Task PushSyncVaultAsync(Guid userId); - Task PushSyncOrganizationsAsync(Guid userId); - Task PushSyncOrgKeysAsync(Guid userId); - Task PushSyncSettingsAsync(Guid userId); - Task PushLogOutAsync(Guid userId, bool excludeCurrentContextFromPush = false); - Task PushSyncSendCreateAsync(Send send); - Task PushSyncSendUpdateAsync(Send send); - Task PushSyncSendDeleteAsync(Send send); - Task PushNotificationAsync(Notification notification); - Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus); - Task PushAuthRequestAsync(AuthRequest authRequest); - Task PushAuthRequestResponseAsync(AuthRequest authRequest); - Task PushSyncOrganizationStatusAsync(Organization organization); - Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization); + Guid InstallationId { get; } + TimeProvider TimeProvider { get; } + ILogger Logger { get; } - Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null); - Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null); - Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null); - Task PushPendingSecurityTasksAsync(Guid userId); + #region Legacy method, to be removed soon. + Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable collectionIds) + => PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds); + + Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable collectionIds) + => PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds); + + Task PushSyncCipherDeleteAsync(Cipher cipher) + => PushCipherAsync(cipher, PushType.SyncLoginDelete, null); + + Task PushSyncFolderCreateAsync(Folder folder) + => PushAsync(new PushNotification + { + Type = PushType.SyncFolderCreate, + Target = NotificationTarget.User, + TargetId = folder.UserId, + Payload = new SyncFolderPushNotification + { + Id = folder.Id, + UserId = folder.UserId, + RevisionDate = folder.RevisionDate, + }, + ExcludeCurrentContext = true, + }); + + Task PushSyncFolderUpdateAsync(Folder folder) + => PushAsync(new PushNotification + { + Type = PushType.SyncFolderUpdate, + Target = NotificationTarget.User, + TargetId = folder.UserId, + Payload = new SyncFolderPushNotification + { + Id = folder.Id, + UserId = folder.UserId, + RevisionDate = folder.RevisionDate, + }, + ExcludeCurrentContext = true, + }); + + Task PushSyncFolderDeleteAsync(Folder folder) + => PushAsync(new PushNotification + { + Type = PushType.SyncFolderDelete, + Target = NotificationTarget.User, + TargetId = folder.UserId, + Payload = new SyncFolderPushNotification + { + Id = folder.Id, + UserId = folder.UserId, + RevisionDate = folder.RevisionDate, + }, + ExcludeCurrentContext = true, + }); + + Task PushSyncCiphersAsync(Guid userId) + => PushAsync(new PushNotification + { + Type = PushType.SyncCiphers, + Target = NotificationTarget.User, + TargetId = userId, + Payload = new UserPushNotification + { + UserId = userId, + Date = TimeProvider.GetUtcNow().UtcDateTime, + }, + ExcludeCurrentContext = false, + }); + + Task PushSyncVaultAsync(Guid userId) + => PushAsync(new PushNotification + { + Type = PushType.SyncVault, + Target = NotificationTarget.User, + TargetId = userId, + Payload = new UserPushNotification + { + UserId = userId, + Date = TimeProvider.GetUtcNow().UtcDateTime, + }, + ExcludeCurrentContext = false, + }); + + Task PushSyncOrganizationsAsync(Guid userId) + => PushAsync(new PushNotification + { + Type = PushType.SyncOrganizations, + Target = NotificationTarget.User, + TargetId = userId, + Payload = new UserPushNotification + { + UserId = userId, + Date = TimeProvider.GetUtcNow().UtcDateTime, + }, + ExcludeCurrentContext = false, + }); + + Task PushSyncOrgKeysAsync(Guid userId) + => PushAsync(new PushNotification + { + Type = PushType.SyncOrgKeys, + Target = NotificationTarget.User, + TargetId = userId, + Payload = new UserPushNotification + { + UserId = userId, + Date = TimeProvider.GetUtcNow().UtcDateTime, + }, + ExcludeCurrentContext = false, + }); + + Task PushSyncSettingsAsync(Guid userId) + => PushAsync(new PushNotification + { + Type = PushType.SyncSettings, + Target = NotificationTarget.User, + TargetId = userId, + Payload = new UserPushNotification + { + UserId = userId, + Date = TimeProvider.GetUtcNow().UtcDateTime, + }, + ExcludeCurrentContext = false, + }); + + Task PushLogOutAsync(Guid userId, bool excludeCurrentContextFromPush = false) + => PushAsync(new PushNotification + { + Type = PushType.LogOut, + Target = NotificationTarget.User, + TargetId = userId, + Payload = new UserPushNotification + { + UserId = userId, + Date = TimeProvider.GetUtcNow().UtcDateTime, + }, + ExcludeCurrentContext = excludeCurrentContextFromPush, + }); + + Task PushSyncSendCreateAsync(Send send) + { + if (send.UserId.HasValue) + { + return PushAsync(new PushNotification + { + Type = PushType.SyncSendCreate, + Target = NotificationTarget.User, + TargetId = send.UserId.Value, + Payload = new SyncSendPushNotification + { + Id = send.Id, + UserId = send.UserId.Value, + RevisionDate = send.RevisionDate, + }, + ExcludeCurrentContext = true, + }); + } + + return Task.CompletedTask; + } + + Task PushSyncSendUpdateAsync(Send send) + { + if (send.UserId.HasValue) + { + return PushAsync(new PushNotification + { + Type = PushType.SyncSendUpdate, + Target = NotificationTarget.User, + TargetId = send.UserId.Value, + Payload = new SyncSendPushNotification + { + Id = send.Id, + UserId = send.UserId.Value, + RevisionDate = send.RevisionDate, + }, + ExcludeCurrentContext = true, + }); + } + + return Task.CompletedTask; + } + + Task PushSyncSendDeleteAsync(Send send) + { + if (send.UserId.HasValue) + { + return PushAsync(new PushNotification + { + Type = PushType.SyncSendDelete, + Target = NotificationTarget.User, + TargetId = send.UserId.Value, + Payload = new SyncSendPushNotification + { + Id = send.Id, + UserId = send.UserId.Value, + RevisionDate = send.RevisionDate, + }, + ExcludeCurrentContext = true, + }); + } + + return Task.CompletedTask; + } + + Task PushNotificationAsync(Notification notification) + { + var message = new NotificationPushNotification + { + Id = notification.Id, + Priority = notification.Priority, + Global = notification.Global, + ClientType = notification.ClientType, + UserId = notification.UserId, + OrganizationId = notification.OrganizationId, + InstallationId = notification.Global ? InstallationId : null, + TaskId = notification.TaskId, + Title = notification.Title, + Body = notification.Body, + CreationDate = notification.CreationDate, + RevisionDate = notification.RevisionDate, + }; + + NotificationTarget target; + Guid targetId; + + if (notification.Global) + { + // TODO: Think about this a bit more + target = NotificationTarget.Installation; + targetId = InstallationId; + } + else if (notification.UserId.HasValue) + { + target = NotificationTarget.User; + targetId = notification.UserId.Value; + } + else if (notification.OrganizationId.HasValue) + { + target = NotificationTarget.Organization; + targetId = notification.OrganizationId.Value; + } + else + { + Logger.LogWarning("Invalid notification id {NotificationId} push notification", notification.Id); + return Task.CompletedTask; + } + + return PushAsync(new PushNotification + { + Type = PushType.Notification, + Target = target, + TargetId = targetId, + Payload = message, + ExcludeCurrentContext = true, + ClientType = notification.ClientType, + }); + } + + Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) + { + var message = new NotificationPushNotification + { + Id = notification.Id, + Priority = notification.Priority, + Global = notification.Global, + ClientType = notification.ClientType, + UserId = notification.UserId, + OrganizationId = notification.OrganizationId, + InstallationId = notification.Global ? InstallationId : null, + TaskId = notification.TaskId, + Title = notification.Title, + Body = notification.Body, + CreationDate = notification.CreationDate, + RevisionDate = notification.RevisionDate, + ReadDate = notificationStatus.ReadDate, + DeletedDate = notificationStatus.DeletedDate, + }; + + NotificationTarget target; + Guid targetId; + + if (notification.Global) + { + // TODO: Think about this a bit more + target = NotificationTarget.Installation; + targetId = InstallationId; + } + else if (notification.UserId.HasValue) + { + target = NotificationTarget.User; + targetId = notification.UserId.Value; + } + else if (notification.OrganizationId.HasValue) + { + target = NotificationTarget.Organization; + targetId = notification.OrganizationId.Value; + } + else + { + Logger.LogWarning("Invalid notification status id {NotificationId} push notification", notification.Id); + return Task.CompletedTask; + } + + return PushAsync(new PushNotification + { + Type = PushType.NotificationStatus, + Target = target, + TargetId = targetId, + Payload = message, + ExcludeCurrentContext = true, + ClientType = notification.ClientType, + }); + } + + Task PushAuthRequestAsync(AuthRequest authRequest) + => PushAsync(new PushNotification + { + Type = PushType.AuthRequest, + Target = NotificationTarget.User, + TargetId = authRequest.UserId, + Payload = new AuthRequestPushNotification + { + Id = authRequest.Id, + UserId = authRequest.UserId, + }, + ExcludeCurrentContext = true, + }); + + Task PushAuthRequestResponseAsync(AuthRequest authRequest) + => PushAsync(new PushNotification + { + Type = PushType.AuthRequestResponse, + Target = NotificationTarget.User, + TargetId = authRequest.UserId, + Payload = new AuthRequestPushNotification + { + Id = authRequest.Id, + UserId = authRequest.UserId, + }, + 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 + { + Type = PushType.SyncOrganizationCollectionSettingChanged, + Target = NotificationTarget.Organization, + TargetId = organization.Id, + Payload = new OrganizationCollectionManagementPushNotification + { + OrganizationId = organization.Id, + LimitCollectionCreation = organization.LimitCollectionCreation, + LimitCollectionDeletion = organization.LimitCollectionDeletion, + LimitItemDeletion = organization.LimitItemDeletion, + }, + ExcludeCurrentContext = false, + }); + + Task PushPendingSecurityTasksAsync(Guid userId) + => PushAsync(new PushNotification + { + Type = PushType.PendingSecurityTasks, + Target = NotificationTarget.User, + TargetId = userId, + Payload = new UserPushNotification + { + UserId = userId, + Date = TimeProvider.GetUtcNow().UtcDateTime, + }, + ExcludeCurrentContext = false, + }); + #endregion + + Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable? collectionIds); + + Task PushAsync(PushNotification pushNotification) + where T : class; } diff --git a/src/Core/Platform/Push/Services/IPushRelayer.cs b/src/Core/Platform/Push/Services/IPushRelayer.cs new file mode 100644 index 0000000000..fde0a521f3 --- /dev/null +++ b/src/Core/Platform/Push/Services/IPushRelayer.cs @@ -0,0 +1,44 @@ +#nullable enable + +using System.Text.Json; +using Bit.Core.Enums; + +namespace Bit.Core.Platform.Push.Internal; + +/// +/// An object encapsulating the information that is available in a notification +/// given to us from a self-hosted installation. +/// +public class RelayedNotification +{ + /// + public required PushType Type { get; init; } + /// + public required NotificationTarget Target { get; init; } + /// + public required Guid TargetId { get; init; } + /// + public required JsonElement Payload { get; init; } + /// + public required ClientType? ClientType { get; init; } + public required Guid? DeviceId { get; init; } + public required string? Identifier { get; init; } +} + +/// +/// A service for taking a notification that was relayed to us from a self-hosted installation and +/// will be injested into our infrastructure so that we can get the notification to devices that require +/// cloud interaction. +/// +/// +/// This interface should be treated as internal and not consumed by other teams. +/// +public interface IPushRelayer +{ + /// + /// Relays a notification that was received from an authenticated installation into our cloud push notification infrastructure. + /// + /// The authenticated installation this notification came from. + /// The information received from the self-hosted installation. + Task RelayAsync(Guid fromInstallation, RelayedNotification relayedNotification); +} diff --git a/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs b/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs index 490b690a3b..404b153fa3 100644 --- a/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs +++ b/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs @@ -1,202 +1,77 @@ #nullable enable -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Auth.Entities; using Bit.Core.Enums; -using Bit.Core.NotificationCenter.Entities; using Bit.Core.Settings; -using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Bit.Core.Platform.Push.Internal; public class MultiServicePushNotificationService : IPushNotificationService { - private readonly IEnumerable _services; - private readonly ILogger _logger; + private readonly IEnumerable _services; + + public Guid InstallationId { get; } + + public TimeProvider TimeProvider { get; } + + public ILogger Logger { get; } public MultiServicePushNotificationService( - [FromKeyedServices("implementation")] IEnumerable services, + IEnumerable services, ILogger logger, - GlobalSettings globalSettings) + GlobalSettings globalSettings, + TimeProvider timeProvider) { _services = services; - _logger = logger; - _logger.LogInformation("Hub services: {Services}", _services.Count()); + Logger = logger; + Logger.LogInformation("Hub services: {Services}", _services.Count()); globalSettings.NotificationHubPool?.NotificationHubs?.ForEach(hub => { - _logger.LogInformation("HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}", hub.HubName, hub.EnableSendTracing, hub.RegistrationStartDate, hub.RegistrationEndDate); + Logger.LogInformation("HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}", hub.HubName, hub.EnableSendTracing, hub.RegistrationStartDate, hub.RegistrationEndDate); }); + InstallationId = globalSettings.Installation.Id; + TimeProvider = timeProvider; } - public Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable collectionIds) - { - PushToServices((s) => s.PushSyncCipherCreateAsync(cipher, collectionIds)); - return Task.FromResult(0); - } - - public Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable collectionIds) - { - PushToServices((s) => s.PushSyncCipherUpdateAsync(cipher, collectionIds)); - return Task.FromResult(0); - } - - public Task PushSyncCipherDeleteAsync(Cipher cipher) - { - PushToServices((s) => s.PushSyncCipherDeleteAsync(cipher)); - return Task.FromResult(0); - } - - public Task PushSyncFolderCreateAsync(Folder folder) - { - PushToServices((s) => s.PushSyncFolderCreateAsync(folder)); - return Task.FromResult(0); - } - - public Task PushSyncFolderUpdateAsync(Folder folder) - { - PushToServices((s) => s.PushSyncFolderUpdateAsync(folder)); - return Task.FromResult(0); - } - - public Task PushSyncFolderDeleteAsync(Folder folder) - { - PushToServices((s) => s.PushSyncFolderDeleteAsync(folder)); - return Task.FromResult(0); - } - - public Task PushSyncCiphersAsync(Guid userId) - { - PushToServices((s) => s.PushSyncCiphersAsync(userId)); - return Task.FromResult(0); - } - - public Task PushSyncVaultAsync(Guid userId) - { - PushToServices((s) => s.PushSyncVaultAsync(userId)); - return Task.FromResult(0); - } - - public Task PushSyncOrganizationsAsync(Guid userId) - { - PushToServices((s) => s.PushSyncOrganizationsAsync(userId)); - return Task.FromResult(0); - } - - public Task PushSyncOrgKeysAsync(Guid userId) - { - PushToServices((s) => s.PushSyncOrgKeysAsync(userId)); - return Task.FromResult(0); - } - - public Task PushSyncSettingsAsync(Guid userId) - { - PushToServices((s) => s.PushSyncSettingsAsync(userId)); - return Task.FromResult(0); - } - - public Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false) - { - PushToServices((s) => s.PushLogOutAsync(userId, excludeCurrentContext)); - return Task.FromResult(0); - } - - public Task PushSyncSendCreateAsync(Send send) - { - PushToServices((s) => s.PushSyncSendCreateAsync(send)); - return Task.FromResult(0); - } - - public Task PushSyncSendUpdateAsync(Send send) - { - PushToServices((s) => s.PushSyncSendUpdateAsync(send)); - return Task.FromResult(0); - } - - public Task PushAuthRequestAsync(AuthRequest authRequest) - { - PushToServices((s) => s.PushAuthRequestAsync(authRequest)); - return Task.FromResult(0); - } - - public Task PushAuthRequestResponseAsync(AuthRequest authRequest) - { - PushToServices((s) => s.PushAuthRequestResponseAsync(authRequest)); - return Task.FromResult(0); - } - - public Task PushSyncSendDeleteAsync(Send send) - { - PushToServices((s) => s.PushSyncSendDeleteAsync(send)); - return Task.FromResult(0); - } - - public Task PushSyncOrganizationStatusAsync(Organization organization) - { - PushToServices((s) => s.PushSyncOrganizationStatusAsync(organization)); - return Task.FromResult(0); - } - - public Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) - { - PushToServices(s => s.PushSyncOrganizationCollectionManagementSettingsAsync(organization)); - return Task.CompletedTask; - } - - public Task PushNotificationAsync(Notification notification) - { - PushToServices((s) => s.PushNotificationAsync(notification)); - return Task.CompletedTask; - } - - public Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) - { - PushToServices((s) => s.PushNotificationStatusAsync(notification, notificationStatus)); - return Task.CompletedTask; - } - - public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null) - { - PushToServices((s) => - s.SendPayloadToInstallationAsync(installationId, type, payload, identifier, deviceId, clientType)); - return Task.CompletedTask; - } - - public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null) - { - PushToServices((s) => s.SendPayloadToUserAsync(userId, type, payload, identifier, deviceId, clientType)); - return Task.FromResult(0); - } - - public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null) - { - PushToServices((s) => s.SendPayloadToOrganizationAsync(orgId, type, payload, identifier, deviceId, clientType)); - return Task.FromResult(0); - } - - public Task PushPendingSecurityTasksAsync(Guid userId) - { - PushToServices((s) => s.PushPendingSecurityTasksAsync(userId)); - return Task.CompletedTask; - } - - private void PushToServices(Func pushFunc) + private Task PushToServices(Func pushFunc) { if (!_services.Any()) { - _logger.LogWarning("No services found to push notification"); - return; + Logger.LogWarning("No services found to push notification"); + return Task.CompletedTask; } + +#if DEBUG + var tasks = new List(); +#endif + foreach (var service in _services) { - _logger.LogDebug("Pushing notification to service {ServiceName}", service.GetType().Name); + Logger.LogDebug("Pushing notification to service {ServiceName}", service.GetType().Name); +#if DEBUG + var task = +#endif pushFunc(service); +#if DEBUG + tasks.Add(task); +#endif } + +#if DEBUG + return Task.WhenAll(tasks); +#else + return Task.CompletedTask; +#endif + } + + public Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable? collectionIds) + { + return PushToServices((s) => s.PushCipherAsync(cipher, pushType, collectionIds)); + } + public Task PushAsync(PushNotification pushNotification) where T : class + { + return PushToServices((s) => s.PushAsync(pushNotification)); } } diff --git a/src/Core/Platform/Push/Services/NoopPushNotificationService.cs b/src/Core/Platform/Push/Services/NoopPushNotificationService.cs index 6e7278cf94..e6f71de006 100644 --- a/src/Core/Platform/Push/Services/NoopPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/NoopPushNotificationService.cs @@ -1,129 +1,12 @@ #nullable enable -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Auth.Entities; using Bit.Core.Enums; -using Bit.Core.NotificationCenter.Entities; -using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; namespace Bit.Core.Platform.Push.Internal; -public class NoopPushNotificationService : IPushNotificationService +internal class NoopPushNotificationService : IPushEngine { - public Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable collectionIds) - { - return Task.FromResult(0); - } + public Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable? collectionIds) => Task.CompletedTask; - public Task PushSyncCipherDeleteAsync(Cipher cipher) - { - return Task.FromResult(0); - } - - public Task PushSyncCiphersAsync(Guid userId) - { - return Task.FromResult(0); - } - - public Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable collectionIds) - { - return Task.FromResult(0); - } - - public Task PushSyncFolderCreateAsync(Folder folder) - { - return Task.FromResult(0); - } - - public Task PushSyncFolderDeleteAsync(Folder folder) - { - return Task.FromResult(0); - } - - public Task PushSyncFolderUpdateAsync(Folder folder) - { - return Task.FromResult(0); - } - - public Task PushSyncOrganizationsAsync(Guid userId) - { - return Task.FromResult(0); - } - - public Task PushSyncOrgKeysAsync(Guid userId) - { - return Task.FromResult(0); - } - - public Task PushSyncSettingsAsync(Guid userId) - { - return Task.FromResult(0); - } - - public Task PushSyncVaultAsync(Guid userId) - { - return Task.FromResult(0); - } - - public Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false) - { - return Task.FromResult(0); - } - - public Task PushSyncSendCreateAsync(Send send) - { - return Task.FromResult(0); - } - - public Task PushSyncSendDeleteAsync(Send send) - { - return Task.FromResult(0); - } - - public Task PushSyncSendUpdateAsync(Send send) - { - return Task.FromResult(0); - } - - public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null) - { - return Task.FromResult(0); - } - - public Task PushSyncOrganizationStatusAsync(Organization organization) - { - return Task.FromResult(0); - } - - public Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) => Task.CompletedTask; - - public Task PushAuthRequestAsync(AuthRequest authRequest) - { - return Task.FromResult(0); - } - - public Task PushAuthRequestResponseAsync(AuthRequest authRequest) - { - return Task.FromResult(0); - } - - public Task PushNotificationAsync(Notification notification) => Task.CompletedTask; - - public Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) => - Task.CompletedTask; - - public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null) => Task.CompletedTask; - - public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null) - { - return Task.FromResult(0); - } - - public Task PushPendingSecurityTasksAsync(Guid userId) - { - return Task.FromResult(0); - } + public Task PushAsync(PushNotification pushNotification) where T : class => Task.CompletedTask; } diff --git a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs index bdeefc0363..5e0d584ba8 100644 --- a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs @@ -1,13 +1,9 @@ #nullable enable -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Auth.Entities; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models; -using Bit.Core.NotificationCenter.Entities; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -20,18 +16,15 @@ namespace Bit.Core.Platform.Push; /// Used by Cloud-Hosted environments. /// Received by AzureQueueHostedService message receiver in Notifications project. /// -public class NotificationsApiPushNotificationService : BaseIdentityClientService, IPushNotificationService +public class NotificationsApiPushNotificationService : BaseIdentityClientService, IPushEngine { - private readonly IGlobalSettings _globalSettings; private readonly IHttpContextAccessor _httpContextAccessor; - private readonly TimeProvider _timeProvider; public NotificationsApiPushNotificationService( IHttpClientFactory httpFactory, GlobalSettings globalSettings, IHttpContextAccessor httpContextAccessor, - ILogger logger, - TimeProvider timeProvider) + ILogger logger) : base( httpFactory, globalSettings.BaseServiceUri.InternalNotifications, @@ -41,27 +34,10 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService globalSettings.InternalIdentityKey, logger) { - _globalSettings = globalSettings; _httpContextAccessor = httpContextAccessor; - _timeProvider = timeProvider; } - public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable collectionIds) - { - await PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds); - } - - public async Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable collectionIds) - { - await PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds); - } - - public async Task PushSyncCipherDeleteAsync(Cipher cipher) - { - await PushCipherAsync(cipher, PushType.SyncLoginDelete, null); - } - - private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable? collectionIds) + public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable? collectionIds) { if (cipher.OrganizationId.HasValue) { @@ -89,174 +65,6 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService } } - public async Task PushSyncFolderCreateAsync(Folder folder) - { - await PushFolderAsync(folder, PushType.SyncFolderCreate); - } - - public async Task PushSyncFolderUpdateAsync(Folder folder) - { - await PushFolderAsync(folder, PushType.SyncFolderUpdate); - } - - public async Task PushSyncFolderDeleteAsync(Folder folder) - { - await PushFolderAsync(folder, PushType.SyncFolderDelete); - } - - private async Task PushFolderAsync(Folder folder, PushType type) - { - var message = new SyncFolderPushNotification - { - Id = folder.Id, - UserId = folder.UserId, - RevisionDate = folder.RevisionDate - }; - - await SendMessageAsync(type, message, true); - } - - public async Task PushSyncCiphersAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncCiphers); - } - - public async Task PushSyncVaultAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncVault); - } - - public async Task PushSyncOrganizationsAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncOrganizations); - } - - public async Task PushSyncOrgKeysAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncOrgKeys); - } - - public async Task PushSyncSettingsAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncSettings); - } - - public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext) - { - await PushUserAsync(userId, PushType.LogOut, excludeCurrentContext); - } - - private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false) - { - var message = new UserPushNotification - { - UserId = userId, - Date = _timeProvider.GetUtcNow().UtcDateTime, - }; - - await SendMessageAsync(type, message, excludeCurrentContext); - } - - public async Task PushAuthRequestAsync(AuthRequest authRequest) - { - await PushAuthRequestAsync(authRequest, PushType.AuthRequest); - } - - public async Task PushAuthRequestResponseAsync(AuthRequest authRequest) - { - await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse); - } - - private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type) - { - var message = new AuthRequestPushNotification - { - Id = authRequest.Id, - UserId = authRequest.UserId - }; - - await SendMessageAsync(type, message, true); - } - - public async Task PushSyncSendCreateAsync(Send send) - { - await PushSendAsync(send, PushType.SyncSendCreate); - } - - public async Task PushSyncSendUpdateAsync(Send send) - { - await PushSendAsync(send, PushType.SyncSendUpdate); - } - - public async Task PushSyncSendDeleteAsync(Send send) - { - await PushSendAsync(send, PushType.SyncSendDelete); - } - - public async Task PushNotificationAsync(Notification notification) - { - var message = new NotificationPushNotification - { - Id = notification.Id, - Priority = notification.Priority, - Global = notification.Global, - ClientType = notification.ClientType, - UserId = notification.UserId, - OrganizationId = notification.OrganizationId, - InstallationId = notification.Global ? _globalSettings.Installation.Id : null, - TaskId = notification.TaskId, - Title = notification.Title, - Body = notification.Body, - CreationDate = notification.CreationDate, - RevisionDate = notification.RevisionDate - }; - - await SendMessageAsync(PushType.Notification, message, true); - } - - public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) - { - var message = new NotificationPushNotification - { - Id = notification.Id, - Priority = notification.Priority, - Global = notification.Global, - ClientType = notification.ClientType, - UserId = notification.UserId, - OrganizationId = notification.OrganizationId, - InstallationId = notification.Global ? _globalSettings.Installation.Id : null, - TaskId = notification.TaskId, - Title = notification.Title, - Body = notification.Body, - CreationDate = notification.CreationDate, - RevisionDate = notification.RevisionDate, - ReadDate = notificationStatus.ReadDate, - DeletedDate = notificationStatus.DeletedDate - }; - - await SendMessageAsync(PushType.NotificationStatus, message, true); - } - - public async Task PushPendingSecurityTasksAsync(Guid userId) - { - await PushUserAsync(userId, PushType.PendingSecurityTasks); - } - - private async Task PushSendAsync(Send send, PushType type) - { - if (send.UserId.HasValue) - { - var message = new SyncSendPushNotification - { - Id = send.Id, - UserId = send.UserId.Value, - RevisionDate = send.RevisionDate - }; - - await SendMessageAsync(type, message, false); - } - } - private async Task SendMessageAsync(PushType type, T payload, bool excludeCurrentContext) { var contextId = GetContextIdentifier(excludeCurrentContext); @@ -276,43 +84,8 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService return currentContext?.DeviceIdentifier; } - public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null) => - // Noop - Task.CompletedTask; - - public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null) + public async Task PushAsync(PushNotification pushNotification) where T : class { - // Noop - return Task.FromResult(0); + await SendMessageAsync(pushNotification.Type, pushNotification.Payload, pushNotification.ExcludeCurrentContext); } - - public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null) - { - // Noop - return Task.FromResult(0); - } - - public async Task PushSyncOrganizationStatusAsync(Organization organization) - { - var message = new OrganizationStatusPushNotification - { - OrganizationId = organization.Id, - Enabled = organization.Enabled - }; - - await SendMessageAsync(PushType.SyncOrganizationStatusChanged, message, false); - } - - public async Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) => - await SendMessageAsync(PushType.SyncOrganizationCollectionSettingChanged, - new OrganizationCollectionManagementPushNotification - { - OrganizationId = organization.Id, - LimitCollectionCreation = organization.LimitCollectionCreation, - LimitCollectionDeletion = organization.LimitCollectionDeletion, - LimitItemDeletion = organization.LimitItemDeletion - }, false); } diff --git a/src/Core/Platform/Push/Services/PushNotification.cs b/src/Core/Platform/Push/Services/PushNotification.cs new file mode 100644 index 0000000000..e1d3f44cd8 --- /dev/null +++ b/src/Core/Platform/Push/Services/PushNotification.cs @@ -0,0 +1,78 @@ +#nullable enable +using Bit.Core.Enums; + +namespace Bit.Core.Platform.Push; + +/// +/// Contains constants for all the available targets for a given notification. +/// +public enum NotificationTarget +{ + /// + /// The target for the notification is a single user. + /// + User, + /// + /// The target for the notification are all the users in an organization. + /// + Organization, + /// + /// The target for the notification are all the organizations, + /// and all the users in that organization for a installation. + /// + Installation, +} + +/// +/// An object containing all the information required for getting a notification +/// to an end users device and the information you want available to that device. +/// +/// The type of the payload. This type is expected to be able to be roundtripped as JSON. +public record PushNotification + where T : class +{ + /// + /// The to be associated with the notification. This is used to route + /// the notification to the correct handler on the client side. Be sure to use the correct payload + /// type for the associated . + /// + public required PushType Type { get; init; } + + /// + /// The target entity type for the notification. + /// + /// + /// When the target type is the + /// property is expected to be a users ID. When it is + /// it should be an organizations id. When it is a + /// it should be an installation id. + /// + public required NotificationTarget Target { get; init; } + + /// + /// The indentifier for the given . + /// + public required Guid TargetId { get; init; } + + /// + /// The payload to be sent with the notification. This object will be JSON serialized. + /// + public required T Payload { get; init; } + + /// + /// When the notification will not include the current context identifier on it, this + /// means that the notification may get handled on the device that this notification could have originated from. + /// + public required bool ExcludeCurrentContext { get; init; } + + /// + /// The type of clients the notification should be sent to, if then + /// is inferred. + /// + public ClientType? ClientType { get; init; } + + internal Guid? GetTargetWhen(NotificationTarget notificationTarget) + { + return Target == notificationTarget ? TargetId : null; + } +} diff --git a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs index 0ede81e719..9f2289b864 100644 --- a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs @@ -1,18 +1,15 @@ #nullable enable -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Auth.Entities; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.IdentityServer; using Bit.Core.Models; using Bit.Core.Models.Api; -using Bit.Core.NotificationCenter.Entities; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Bit.Core.Platform.Push.Internal; @@ -22,20 +19,18 @@ namespace Bit.Core.Platform.Push.Internal; /// Used by Self-Hosted environments. /// Received by PushController endpoint in Api project. /// -public class RelayPushNotificationService : BaseIdentityClientService, IPushNotificationService +public class RelayPushNotificationService : BaseIdentityClientService, IPushEngine { private readonly IDeviceRepository _deviceRepository; - private readonly IGlobalSettings _globalSettings; private readonly IHttpContextAccessor _httpContextAccessor; - private readonly TimeProvider _timeProvider; + public RelayPushNotificationService( IHttpClientFactory httpFactory, IDeviceRepository deviceRepository, GlobalSettings globalSettings, IHttpContextAccessor httpContextAccessor, - ILogger logger, - TimeProvider timeProvider) + ILogger logger) : base( httpFactory, globalSettings.PushRelayBaseUri, @@ -46,27 +41,10 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti logger) { _deviceRepository = deviceRepository; - _globalSettings = globalSettings; _httpContextAccessor = httpContextAccessor; - _timeProvider = timeProvider; } - public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable collectionIds) - { - await PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds); - } - - public async Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable collectionIds) - { - await PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds); - } - - public async Task PushSyncCipherDeleteAsync(Cipher cipher) - { - await PushCipherAsync(cipher, PushType.SyncLoginDelete, null); - } - - private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable? collectionIds) + public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable? collectionIds) { if (cipher.OrganizationId.HasValue) { @@ -87,306 +65,45 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti RevisionDate = cipher.RevisionDate, }; - await SendPayloadToUserAsync(cipher.UserId.Value, type, message, true); - } - } - - public async Task PushSyncFolderCreateAsync(Folder folder) - { - await PushFolderAsync(folder, PushType.SyncFolderCreate); - } - - public async Task PushSyncFolderUpdateAsync(Folder folder) - { - await PushFolderAsync(folder, PushType.SyncFolderUpdate); - } - - public async Task PushSyncFolderDeleteAsync(Folder folder) - { - await PushFolderAsync(folder, PushType.SyncFolderDelete); - } - - private async Task PushFolderAsync(Folder folder, PushType type) - { - var message = new SyncFolderPushNotification - { - Id = folder.Id, - UserId = folder.UserId, - RevisionDate = folder.RevisionDate - }; - - await SendPayloadToUserAsync(folder.UserId, type, message, true); - } - - public async Task PushSyncCiphersAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncCiphers); - } - - public async Task PushSyncVaultAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncVault); - } - - public async Task PushSyncOrganizationsAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncOrganizations); - } - - public async Task PushSyncOrgKeysAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncOrgKeys); - } - - public async Task PushSyncSettingsAsync(Guid userId) - { - await PushUserAsync(userId, PushType.SyncSettings); - } - - public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false) - { - await PushUserAsync(userId, PushType.LogOut, excludeCurrentContext); - } - - private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false) - { - var message = new UserPushNotification { UserId = userId, Date = _timeProvider.GetUtcNow().UtcDateTime }; - - await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext); - } - - public async Task PushSyncSendCreateAsync(Send send) - { - await PushSendAsync(send, PushType.SyncSendCreate); - } - - public async Task PushSyncSendUpdateAsync(Send send) - { - await PushSendAsync(send, PushType.SyncSendUpdate); - } - - public async Task PushSyncSendDeleteAsync(Send send) - { - await PushSendAsync(send, PushType.SyncSendDelete); - } - - private async Task PushSendAsync(Send send, PushType type) - { - if (send.UserId.HasValue) - { - var message = new SyncSendPushNotification + await PushAsync(new PushNotification { - Id = send.Id, - UserId = send.UserId.Value, - RevisionDate = send.RevisionDate - }; - - await SendPayloadToUserAsync(message.UserId, type, message, true); + Type = type, + Target = NotificationTarget.User, + TargetId = cipher.UserId.Value, + Payload = message, + ExcludeCurrentContext = true, + }); } } - public async Task PushAuthRequestAsync(AuthRequest authRequest) + public async Task PushAsync(PushNotification pushNotification) + where T : class { - await PushAuthRequestAsync(authRequest, PushType.AuthRequest); - } + var deviceIdentifier = _httpContextAccessor.HttpContext + ?.RequestServices.GetService() + ?.DeviceIdentifier; - public async Task PushAuthRequestResponseAsync(AuthRequest authRequest) - { - await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse); - } + Guid? deviceId = null; - private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type) - { - var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId }; - - await SendPayloadToUserAsync(authRequest.UserId, type, message, true); - } - - public async Task PushNotificationAsync(Notification notification) - { - var message = new NotificationPushNotification + if (!string.IsNullOrEmpty(deviceIdentifier)) { - Id = notification.Id, - Priority = notification.Priority, - Global = notification.Global, - ClientType = notification.ClientType, - UserId = notification.UserId, - OrganizationId = notification.OrganizationId, - InstallationId = notification.Global ? _globalSettings.Installation.Id : null, - TaskId = notification.TaskId, - Title = notification.Title, - Body = notification.Body, - CreationDate = notification.CreationDate, - RevisionDate = notification.RevisionDate + var device = await _deviceRepository.GetByIdentifierAsync(deviceIdentifier); + deviceId = device?.Id; + } + + var payload = new PushSendRequestModel + { + Type = pushNotification.Type, + UserId = pushNotification.GetTargetWhen(NotificationTarget.User), + OrganizationId = pushNotification.GetTargetWhen(NotificationTarget.Organization), + InstallationId = pushNotification.GetTargetWhen(NotificationTarget.Installation), + Payload = pushNotification.Payload, + Identifier = pushNotification.ExcludeCurrentContext ? deviceIdentifier : null, + // We set the device id regardless of if they want to exclude the current context or not + DeviceId = deviceId, + ClientType = pushNotification.ClientType, }; - if (notification.Global) - { - await SendPayloadToInstallationAsync(PushType.Notification, message, true, notification.ClientType); - } - else if (notification.UserId.HasValue) - { - await SendPayloadToUserAsync(notification.UserId.Value, PushType.Notification, message, true, - notification.ClientType); - } - else if (notification.OrganizationId.HasValue) - { - await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.Notification, message, - true, notification.ClientType); - } - else - { - _logger.LogWarning("Invalid notification id {NotificationId} push notification", notification.Id); - } - } - - public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) - { - var message = new NotificationPushNotification - { - Id = notification.Id, - Priority = notification.Priority, - Global = notification.Global, - ClientType = notification.ClientType, - UserId = notification.UserId, - OrganizationId = notification.OrganizationId, - InstallationId = notification.Global ? _globalSettings.Installation.Id : null, - TaskId = notification.TaskId, - Title = notification.Title, - Body = notification.Body, - CreationDate = notification.CreationDate, - RevisionDate = notification.RevisionDate, - ReadDate = notificationStatus.ReadDate, - DeletedDate = notificationStatus.DeletedDate - }; - - if (notification.Global) - { - await SendPayloadToInstallationAsync(PushType.NotificationStatus, message, true, notification.ClientType); - } - else if (notification.UserId.HasValue) - { - await SendPayloadToUserAsync(notification.UserId.Value, PushType.NotificationStatus, message, true, - notification.ClientType); - } - else if (notification.OrganizationId.HasValue) - { - await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.NotificationStatus, message, - true, notification.ClientType); - } - else - { - _logger.LogWarning("Invalid notification status id {NotificationId} push notification", notification.Id); - } - } - - public async Task PushSyncOrganizationStatusAsync(Organization organization) - { - var message = new OrganizationStatusPushNotification - { - OrganizationId = organization.Id, - Enabled = organization.Enabled - }; - - await SendPayloadToOrganizationAsync(organization.Id, PushType.SyncOrganizationStatusChanged, message, false); - } - - public async Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) => - await SendPayloadToOrganizationAsync( - organization.Id, - PushType.SyncOrganizationCollectionSettingChanged, - new OrganizationCollectionManagementPushNotification - { - OrganizationId = organization.Id, - LimitCollectionCreation = organization.LimitCollectionCreation, - LimitCollectionDeletion = organization.LimitCollectionDeletion, - LimitItemDeletion = organization.LimitItemDeletion - }, - false - ); - - public async Task PushPendingSecurityTasksAsync(Guid userId) - { - await PushUserAsync(userId, PushType.PendingSecurityTasks); - } - - private async Task SendPayloadToInstallationAsync(PushType type, object payload, bool excludeCurrentContext, - ClientType? clientType = null) - { - var request = new PushSendRequestModel - { - InstallationId = _globalSettings.Installation.Id.ToString(), - Type = type, - Payload = payload, - ClientType = clientType - }; - - await AddCurrentContextAsync(request, excludeCurrentContext); - await SendAsync(HttpMethod.Post, "push/send", request); - } - - private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext, - ClientType? clientType = null) - { - var request = new PushSendRequestModel - { - UserId = userId.ToString(), - Type = type, - Payload = payload, - ClientType = clientType - }; - - await AddCurrentContextAsync(request, excludeCurrentContext); - await SendAsync(HttpMethod.Post, "push/send", request); - } - - private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload, - bool excludeCurrentContext, ClientType? clientType = null) - { - var request = new PushSendRequestModel - { - OrganizationId = orgId.ToString(), - Type = type, - Payload = payload, - ClientType = clientType - }; - - await AddCurrentContextAsync(request, excludeCurrentContext); - await SendAsync(HttpMethod.Post, "push/send", request); - } - - private async Task AddCurrentContextAsync(PushSendRequestModel request, bool addIdentifier) - { - var currentContext = - _httpContextAccessor.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext; - if (!string.IsNullOrWhiteSpace(currentContext?.DeviceIdentifier)) - { - var device = await _deviceRepository.GetByIdentifierAsync(currentContext.DeviceIdentifier); - if (device != null) - { - request.DeviceId = device.Id.ToString(); - } - - if (addIdentifier) - { - request.Identifier = currentContext.DeviceIdentifier; - } - } - } - - public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null) => - throw new NotImplementedException(); - - public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null) - { - throw new NotImplementedException(); - } - - public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier, - string? deviceId = null, ClientType? clientType = null) - { - throw new NotImplementedException(); + await SendAsync(HttpMethod.Post, "push/send", payload); } } diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index f8f8381cc0..871a1be038 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -288,7 +288,7 @@ public static class ServiceCollectionExtensions if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) && CoreHelpers.SettingHasValue(globalSettings.Installation.Key)) { - services.AddKeyedSingleton("implementation"); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.AddSingleton(); } else @@ -299,20 +299,20 @@ public static class ServiceCollectionExtensions if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) && CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications)) { - services.AddKeyedSingleton("implementation"); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); } } else { services.AddSingleton(); services.AddSingleton(); - services.AddKeyedSingleton("implementation"); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddSingleton(); if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString)) { services.AddKeyedSingleton("notifications", (_, _) => new QueueClient(globalSettings.Notifications.ConnectionString, "notifications")); - services.AddKeyedSingleton( - "implementation"); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); } } @@ -366,7 +366,6 @@ public static class ServiceCollectionExtensions { services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/test/Api.IntegrationTest/Factories/ApiApplicationFactory.cs b/test/Api.IntegrationTest/Factories/ApiApplicationFactory.cs index 08c5973936..173580ad8c 100644 --- a/test/Api.IntegrationTest/Factories/ApiApplicationFactory.cs +++ b/test/Api.IntegrationTest/Factories/ApiApplicationFactory.cs @@ -28,6 +28,8 @@ public class ApiApplicationFactory : WebApplicationFactoryBase _identityApplicationFactory.ManagesDatabase = false; } + public IdentityApplicationFactory Identity => _identityApplicationFactory; + protected override void ConfigureWebHost(IWebHostBuilder builder) { base.ConfigureWebHost(builder); diff --git a/test/Api.IntegrationTest/Platform/Controllers/PushControllerTests.cs b/test/Api.IntegrationTest/Platform/Controllers/PushControllerTests.cs new file mode 100644 index 0000000000..4d86817a11 --- /dev/null +++ b/test/Api.IntegrationTest/Platform/Controllers/PushControllerTests.cs @@ -0,0 +1,449 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Text.Json.Nodes; +using Azure.Storage.Queues; +using Bit.Api.IntegrationTest.Factories; +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.Repositories; +using NSubstitute; +using Xunit; +using static Bit.Core.Settings.GlobalSettings; + +namespace Bit.Api.IntegrationTest.Platform.Controllers; + +public class PushControllerTests +{ + private static readonly Guid _userId = Guid.NewGuid(); + private static readonly Guid _organizationId = Guid.NewGuid(); + private static readonly Guid _deviceId = Guid.NewGuid(); + + public static IEnumerable SendData() + { + static object[] Typed(PushSendRequestModel pushSendRequestModel, string expectedHubTagExpression, bool expectHubCall = true) + { + return [pushSendRequestModel, expectedHubTagExpression, expectHubCall]; + } + + static object[] UserTyped(PushType pushType) + { + return Typed(new PushSendRequestModel + { + Type = pushType, + UserId = _userId, + DeviceId = _deviceId, + Payload = new UserPushNotification + { + Date = DateTime.UtcNow, + UserId = _userId, + }, + }, $"(template:payload_userId:%installation%_{_userId})"); + } + + // User cipher + yield return Typed(new PushSendRequestModel + { + Type = PushType.SyncCipherUpdate, + UserId = _userId, + DeviceId = _deviceId, + Payload = new SyncCipherPushNotification + { + Id = Guid.NewGuid(), + UserId = _userId, + }, + }, $"(template:payload_userId:%installation%_{_userId})"); + + // Organization cipher, an org cipher would not naturally be synced from our + // code but it is technically possible to be submitted to the endpoint. + yield return Typed(new PushSendRequestModel + { + Type = PushType.SyncCipherUpdate, + OrganizationId = _organizationId, + DeviceId = _deviceId, + Payload = new SyncCipherPushNotification + { + Id = Guid.NewGuid(), + OrganizationId = _organizationId, + }, + }, $"(template:payload && organizationId:%installation%_{_organizationId})"); + + yield return Typed(new PushSendRequestModel + { + Type = PushType.SyncCipherCreate, + UserId = _userId, + DeviceId = _deviceId, + Payload = new SyncCipherPushNotification + { + Id = Guid.NewGuid(), + UserId = _userId, + }, + }, $"(template:payload_userId:%installation%_{_userId})"); + + // Organization cipher, an org cipher would not naturally be synced from our + // code but it is technically possible to be submitted to the endpoint. + yield return Typed(new PushSendRequestModel + { + Type = PushType.SyncCipherCreate, + OrganizationId = _organizationId, + DeviceId = _deviceId, + Payload = new SyncCipherPushNotification + { + Id = Guid.NewGuid(), + OrganizationId = _organizationId, + }, + }, $"(template:payload && organizationId:%installation%_{_organizationId})"); + + yield return Typed(new PushSendRequestModel + { + Type = PushType.SyncCipherDelete, + UserId = _userId, + DeviceId = _deviceId, + Payload = new SyncCipherPushNotification + { + Id = Guid.NewGuid(), + UserId = _userId, + }, + }, $"(template:payload_userId:%installation%_{_userId})"); + + // Organization cipher, an org cipher would not naturally be synced from our + // code but it is technically possible to be submitted to the endpoint. + yield return Typed(new PushSendRequestModel + { + Type = PushType.SyncCipherDelete, + OrganizationId = _organizationId, + DeviceId = _deviceId, + Payload = new SyncCipherPushNotification + { + Id = Guid.NewGuid(), + OrganizationId = _organizationId, + }, + }, $"(template:payload && organizationId:%installation%_{_organizationId})"); + + yield return Typed(new PushSendRequestModel + { + Type = PushType.SyncFolderDelete, + UserId = _userId, + DeviceId = _deviceId, + Payload = new SyncFolderPushNotification + { + Id = Guid.NewGuid(), + UserId = _userId, + }, + }, $"(template:payload_userId:%installation%_{_userId})"); + + yield return Typed(new PushSendRequestModel + { + Type = PushType.SyncFolderCreate, + UserId = _userId, + DeviceId = _deviceId, + Payload = new SyncFolderPushNotification + { + Id = Guid.NewGuid(), + UserId = _userId, + }, + }, $"(template:payload_userId:%installation%_{_userId})"); + + yield return Typed(new PushSendRequestModel + { + Type = PushType.SyncFolderCreate, + UserId = _userId, + DeviceId = _deviceId, + Payload = new SyncFolderPushNotification + { + Id = Guid.NewGuid(), + UserId = _userId, + }, + }, $"(template:payload_userId:%installation%_{_userId})"); + + yield return UserTyped(PushType.SyncCiphers); + yield return UserTyped(PushType.SyncVault); + yield return UserTyped(PushType.SyncOrganizations); + yield return UserTyped(PushType.SyncOrgKeys); + yield return UserTyped(PushType.SyncSettings); + yield return UserTyped(PushType.LogOut); + yield return UserTyped(PushType.PendingSecurityTasks); + + yield return Typed(new PushSendRequestModel + { + Type = PushType.AuthRequest, + UserId = _userId, + DeviceId = _deviceId, + Payload = new AuthRequestPushNotification + { + Id = Guid.NewGuid(), + UserId = _userId, + }, + }, $"(template:payload_userId:%installation%_{_userId})"); + + yield return Typed(new PushSendRequestModel + { + Type = PushType.AuthRequestResponse, + UserId = _userId, + DeviceId = _deviceId, + Payload = new AuthRequestPushNotification + { + Id = Guid.NewGuid(), + UserId = _userId, + }, + }, $"(template:payload_userId:%installation%_{_userId})"); + + yield return Typed(new PushSendRequestModel + { + Type = PushType.Notification, + UserId = _userId, + DeviceId = _deviceId, + Payload = new NotificationPushNotification + { + Id = Guid.NewGuid(), + UserId = _userId, + }, + }, $"(template:payload_userId:%installation%_{_userId})"); + + yield return Typed(new PushSendRequestModel + { + Type = PushType.Notification, + UserId = _userId, + DeviceId = _deviceId, + ClientType = ClientType.All, + Payload = new NotificationPushNotification + { + Id = Guid.NewGuid(), + Global = true, + }, + }, $"(template:payload_userId:%installation%_{_userId})"); + + yield return Typed(new PushSendRequestModel + { + Type = PushType.NotificationStatus, + OrganizationId = _organizationId, + DeviceId = _deviceId, + Payload = new NotificationPushNotification + { + Id = Guid.NewGuid(), + UserId = _userId, + }, + }, $"(template:payload && organizationId:%installation%_{_organizationId})"); + + yield return Typed(new PushSendRequestModel + { + Type = PushType.NotificationStatus, + OrganizationId = _organizationId, + DeviceId = _deviceId, + Payload = new NotificationPushNotification + { + Id = Guid.NewGuid(), + UserId = _userId, + }, + }, $"(template:payload && organizationId:%installation%_{_organizationId})"); + } + + [Theory] + [MemberData(nameof(SendData))] + public async Task Send_Works(PushSendRequestModel pushSendRequestModel, string expectedHubTagExpression, bool expectHubCall) + { + var (apiFactory, httpClient, installation, queueClient, notificationHubProxy) = await SetupTest(); + + // Act + var pushSendResponse = await httpClient.PostAsJsonAsync("push/send", pushSendRequestModel); + + // Assert + pushSendResponse.EnsureSuccessStatusCode(); + + // Relayed notifications, the ones coming to this endpoint should + // not make their way into our Azure Queue and instead should only be sent to Azure Notifications + // hub. + await queueClient + .Received(0) + .SendMessageAsync(Arg.Any()); + + // Check that this notification was sent through hubs the expected number of times + await notificationHubProxy + .Received(expectHubCall ? 1 : 0) + .SendTemplateNotificationAsync( + Arg.Any>(), + Arg.Is(expectedHubTagExpression.Replace("%installation%", installation.Id.ToString())) + ); + + // TODO: Expect on the dictionary more? + + // Notifications being relayed from SH should have the device id + // tracked so that we can later send the notification to that device. + await apiFactory.GetService() + .Received(1) + .UpsertAsync(Arg.Is( + ide => ide.PartitionKey == installation.Id.ToString() && ide.RowKey == pushSendRequestModel.DeviceId.ToString() + )); + } + + [Fact] + public async Task Send_InstallationNotification_NotAuthenticatedInstallation_Fails() + { + var (_, httpClient, _, _, _) = await SetupTest(); + + var response = await httpClient.PostAsJsonAsync("push/send", new PushSendRequestModel + { + Type = PushType.NotificationStatus, + InstallationId = Guid.NewGuid(), + Payload = new { } + }); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var body = await response.Content.ReadFromJsonAsync(); + Assert.Equal(JsonValueKind.Object, body.GetValueKind()); + Assert.True(body.AsObject().TryGetPropertyValue("message", out var message)); + Assert.Equal(JsonValueKind.String, message.GetValueKind()); + Assert.Equal("InstallationId does not match current context.", message.GetValue()); + } + + [Fact] + public async Task Send_InstallationNotification_Works() + { + var (apiFactory, httpClient, installation, _, notificationHubProxy) = await SetupTest(); + + var deviceId = Guid.NewGuid(); + + var response = await httpClient.PostAsJsonAsync("push/send", new PushSendRequestModel + { + Type = PushType.NotificationStatus, + InstallationId = installation.Id, + Payload = new { }, + DeviceId = deviceId, + ClientType = ClientType.Web, + }); + + response.EnsureSuccessStatusCode(); + + await notificationHubProxy + .Received(1) + .SendTemplateNotificationAsync( + Arg.Any>(), + Arg.Is($"(template:payload && installationId:{installation.Id} && clientType:Web)") + ); + + await apiFactory.GetService() + .Received(1) + .UpsertAsync(Arg.Is( + ide => ide.PartitionKey == installation.Id.ToString() && ide.RowKey == deviceId.ToString() + )); + } + + [Fact] + public async Task Send_NoOrganizationNoInstallationNoUser_FailsModelValidation() + { + var (_, client, _, _, _) = await SetupTest(); + + var response = await client.PostAsJsonAsync("push/send", new PushSendRequestModel + { + Type = PushType.AuthRequest, + Payload = new { }, + }); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var body = await response.Content.ReadFromJsonAsync(); + Assert.Equal(JsonValueKind.Object, body.GetValueKind()); + Assert.True(body.AsObject().TryGetPropertyValue("message", out var message)); + Assert.Equal(JsonValueKind.String, message.GetValueKind()); + Assert.Equal("The model state is invalid.", message.GetValue()); + } + + private static async Task<(ApiApplicationFactory Factory, HttpClient AuthedClient, Installation Installation, QueueClient MockedQueue, INotificationHubProxy MockedHub)> SetupTest() + { + // Arrange + var apiFactory = new ApiApplicationFactory(); + + var queueClient = Substitute.For(); + + // Substitute the underlying queue messages will go to. + apiFactory.ConfigureServices(services => + { + var queueClientService = services.FirstOrDefault( + sd => sd.ServiceKey == (object)"notifications" + && sd.ServiceType == typeof(QueueClient) + ) ?? throw new InvalidOperationException("Expected service was not found."); + + services.Remove(queueClientService); + + services.AddKeyedSingleton("notifications", queueClient); + }); + + var notificationHubProxy = Substitute.For(); + + apiFactory.SubstituteService(s => + { + s.AllClients + .Returns(notificationHubProxy); + }); + + apiFactory.SubstituteService(s => { }); + + // Setup as cloud with NotificationHub setup and Azure Queue + apiFactory.UpdateConfiguration("GlobalSettings:Notifications:ConnectionString", "any_value"); + + // Configure hubs + var index = 0; + void AddHub(NotificationHubSettings notificationHubSettings) + { + apiFactory.UpdateConfiguration( + $"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:ConnectionString", + notificationHubSettings.ConnectionString + ); + apiFactory.UpdateConfiguration( + $"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:HubName", + notificationHubSettings.HubName + ); + apiFactory.UpdateConfiguration( + $"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:RegistrationStartDate", + notificationHubSettings.RegistrationStartDate?.ToString() + ); + apiFactory.UpdateConfiguration( + $"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:RegistrationEndDate", + notificationHubSettings.RegistrationEndDate?.ToString() + ); + index++; + } + + AddHub(new NotificationHubSettings + { + ConnectionString = "some_value", + RegistrationStartDate = DateTime.UtcNow.AddDays(-2), + }); + + var httpClient = apiFactory.CreateClient(); + + // Add installation into database + var installationRepository = apiFactory.GetService(); + var installation = await installationRepository.CreateAsync(new Installation + { + Key = "my_test_key", + Email = "test@example.com", + Enabled = true, + }); + + var identityClient = apiFactory.Identity.CreateDefaultClient(); + + var connectTokenResponse = await identityClient.PostAsync("connect/token", new FormUrlEncodedContent(new Dictionary + { + { "grant_type", "client_credentials" }, + { "scope", "api.push" }, + { "client_id", $"installation.{installation.Id}" }, + { "client_secret", installation.Key }, + })); + + connectTokenResponse.EnsureSuccessStatusCode(); + + var connectTokenResponseModel = await connectTokenResponse.Content.ReadFromJsonAsync(); + + // Setup authentication + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( + connectTokenResponseModel["token_type"].GetValue(), + connectTokenResponseModel["access_token"].GetValue() + ); + + return (apiFactory, httpClient, installation, queueClient, notificationHubProxy); + } +} diff --git a/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs b/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs index 6df09c17dc..d6a26255e9 100644 --- a/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs +++ b/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs @@ -18,210 +18,6 @@ namespace Bit.Api.Test.Platform.Push.Controllers; [SutProviderCustomize] public class PushControllerTests { - [Theory] - [BitAutoData(false, true)] - [BitAutoData(false, false)] - [BitAutoData(true, true)] - public async Task SendAsync_InstallationIdNotSetOrSelfHosted_BadRequest(bool haveInstallationId, bool selfHosted, - SutProvider sutProvider, Guid installationId, Guid userId, Guid organizationId) - { - sutProvider.GetDependency().SelfHosted = selfHosted; - if (haveInstallationId) - { - sutProvider.GetDependency().InstallationId.Returns(installationId); - } - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.SendAsync(new PushSendRequestModel - { - Type = PushType.Notification, - UserId = userId.ToString(), - OrganizationId = organizationId.ToString(), - InstallationId = installationId.ToString(), - Payload = "test-payload" - })); - - Assert.Equal("Not correctly configured for push relays.", exception.Message); - - await sutProvider.GetDependency().Received(0) - .SendPayloadToUserAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any()); - await sutProvider.GetDependency().Received(0) - .SendPayloadToOrganizationAsync(Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()); - await sutProvider.GetDependency().Received(0) - .SendPayloadToInstallationAsync(Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory] - [BitAutoData] - public async Task SendAsync_UserIdAndOrganizationIdAndInstallationIdEmpty_NoPushNotificationSent( - SutProvider sutProvider, Guid installationId) - { - sutProvider.GetDependency().SelfHosted = false; - sutProvider.GetDependency().InstallationId.Returns(installationId); - - await sutProvider.Sut.SendAsync(new PushSendRequestModel - { - Type = PushType.Notification, - UserId = null, - OrganizationId = null, - InstallationId = null, - Payload = "test-payload" - }); - - await sutProvider.GetDependency().Received(0) - .SendPayloadToUserAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any()); - await sutProvider.GetDependency().Received(0) - .SendPayloadToOrganizationAsync(Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()); - await sutProvider.GetDependency().Received(0) - .SendPayloadToInstallationAsync(Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory] - [RepeatingPatternBitAutoData([false, true], [false, true], [false, true])] - public async Task SendAsync_UserIdSet_SendPayloadToUserAsync(bool haveIdentifier, bool haveDeviceId, - bool haveOrganizationId, SutProvider sutProvider, Guid installationId, Guid userId, - Guid identifier, Guid deviceId) - { - sutProvider.GetDependency().SelfHosted = false; - sutProvider.GetDependency().InstallationId.Returns(installationId); - - var expectedUserId = $"{installationId}_{userId}"; - var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null; - var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null; - - await sutProvider.Sut.SendAsync(new PushSendRequestModel - { - Type = PushType.Notification, - UserId = userId.ToString(), - OrganizationId = haveOrganizationId ? Guid.NewGuid().ToString() : null, - InstallationId = null, - Payload = "test-payload", - DeviceId = haveDeviceId ? deviceId.ToString() : null, - Identifier = haveIdentifier ? identifier.ToString() : null, - ClientType = ClientType.All, - }); - - await sutProvider.GetDependency().Received(1) - .SendPayloadToUserAsync(expectedUserId, PushType.Notification, "test-payload", expectedIdentifier, - expectedDeviceId, ClientType.All); - await sutProvider.GetDependency().Received(0) - .SendPayloadToOrganizationAsync(Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()); - await sutProvider.GetDependency().Received(0) - .SendPayloadToInstallationAsync(Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory] - [RepeatingPatternBitAutoData([false, true], [false, true])] - public async Task SendAsync_OrganizationIdSet_SendPayloadToOrganizationAsync(bool haveIdentifier, bool haveDeviceId, - SutProvider sutProvider, Guid installationId, Guid organizationId, Guid identifier, - Guid deviceId) - { - sutProvider.GetDependency().SelfHosted = false; - sutProvider.GetDependency().InstallationId.Returns(installationId); - - var expectedOrganizationId = $"{installationId}_{organizationId}"; - var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null; - var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null; - - await sutProvider.Sut.SendAsync(new PushSendRequestModel - { - Type = PushType.Notification, - UserId = null, - OrganizationId = organizationId.ToString(), - InstallationId = null, - Payload = "test-payload", - DeviceId = haveDeviceId ? deviceId.ToString() : null, - Identifier = haveIdentifier ? identifier.ToString() : null, - ClientType = ClientType.All, - }); - - await sutProvider.GetDependency().Received(1) - .SendPayloadToOrganizationAsync(expectedOrganizationId, PushType.Notification, "test-payload", - expectedIdentifier, expectedDeviceId, ClientType.All); - await sutProvider.GetDependency().Received(0) - .SendPayloadToUserAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any()); - await sutProvider.GetDependency().Received(0) - .SendPayloadToInstallationAsync(Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory] - [RepeatingPatternBitAutoData([false, true], [false, true])] - public async Task SendAsync_InstallationIdSet_SendPayloadToInstallationAsync(bool haveIdentifier, bool haveDeviceId, - SutProvider sutProvider, Guid installationId, Guid identifier, Guid deviceId) - { - sutProvider.GetDependency().SelfHosted = false; - sutProvider.GetDependency().InstallationId.Returns(installationId); - - var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null; - var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null; - - await sutProvider.Sut.SendAsync(new PushSendRequestModel - { - Type = PushType.Notification, - UserId = null, - OrganizationId = null, - InstallationId = installationId.ToString(), - Payload = "test-payload", - DeviceId = haveDeviceId ? deviceId.ToString() : null, - Identifier = haveIdentifier ? identifier.ToString() : null, - ClientType = ClientType.All, - }); - - await sutProvider.GetDependency().Received(1) - .SendPayloadToInstallationAsync(installationId.ToString(), PushType.Notification, "test-payload", - expectedIdentifier, expectedDeviceId, ClientType.All); - await sutProvider.GetDependency().Received(0) - .SendPayloadToOrganizationAsync(Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()); - await sutProvider.GetDependency().Received(0) - .SendPayloadToUserAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any()); - } - - [Theory] - [BitAutoData] - public async Task SendAsync_InstallationIdNotMatching_BadRequest(SutProvider sutProvider, - Guid installationId) - { - sutProvider.GetDependency().SelfHosted = false; - sutProvider.GetDependency().InstallationId.Returns(installationId); - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.SendAsync(new PushSendRequestModel - { - Type = PushType.Notification, - UserId = null, - OrganizationId = null, - InstallationId = Guid.NewGuid().ToString(), - Payload = "test-payload", - DeviceId = null, - Identifier = null, - ClientType = ClientType.All, - })); - - Assert.Equal("InstallationId does not match current context.", exception.Message); - - await sutProvider.GetDependency().Received(0) - .SendPayloadToInstallationAsync(Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()); - await sutProvider.GetDependency().Received(0) - .SendPayloadToOrganizationAsync(Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()); - await sutProvider.GetDependency().Received(0) - .SendPayloadToUserAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any()); - } - [Theory] [BitAutoData(false, true)] [BitAutoData(false, false)] diff --git a/test/Core.Test/Models/Api/Request/PushSendRequestModelTests.cs b/test/Core.Test/Models/Api/Request/PushSendRequestModelTests.cs index 2d3dbffcf6..e372899599 100644 --- a/test/Core.Test/Models/Api/Request/PushSendRequestModelTests.cs +++ b/test/Core.Test/Models/Api/Request/PushSendRequestModelTests.cs @@ -11,16 +11,14 @@ namespace Bit.Core.Test.Models.Api.Request; public class PushSendRequestModelTests { - [Theory] - [RepeatingPatternBitAutoData([null, "", " "], [null, "", " "], [null, "", " "])] - public void Validate_UserIdOrganizationIdInstallationIdNullOrEmpty_Invalid(string? userId, string? organizationId, - string? installationId) + [Fact] + public void Validate_UserIdOrganizationIdInstallationIdNull_Invalid() { - var model = new PushSendRequestModel + var model = new PushSendRequestModel { - UserId = userId, - OrganizationId = organizationId, - InstallationId = installationId, + UserId = null, + OrganizationId = null, + InstallationId = null, Type = PushType.SyncCiphers, Payload = "test" }; @@ -32,16 +30,14 @@ public class PushSendRequestModelTests result => result.ErrorMessage == "UserId or OrganizationId or InstallationId is required."); } - [Theory] - [RepeatingPatternBitAutoData([null, "", " "], [null, "", " "])] - public void Validate_UserIdProvidedOrganizationIdInstallationIdNullOrEmpty_Valid(string? organizationId, - string? installationId) + [Fact] + public void Validate_UserIdProvidedOrganizationIdInstallationIdNull_Valid() { - var model = new PushSendRequestModel + var model = new PushSendRequestModel { - UserId = Guid.NewGuid().ToString(), - OrganizationId = organizationId, - InstallationId = installationId, + UserId = Guid.NewGuid(), + OrganizationId = null, + InstallationId = null, Type = PushType.SyncCiphers, Payload = "test" }; @@ -51,16 +47,14 @@ public class PushSendRequestModelTests Assert.Empty(results); } - [Theory] - [RepeatingPatternBitAutoData([null, "", " "], [null, "", " "])] - public void Validate_OrganizationIdProvidedUserIdInstallationIdNullOrEmpty_Valid(string? userId, - string? installationId) + [Fact] + public void Validate_OrganizationIdProvidedUserIdInstallationIdNull_Valid() { - var model = new PushSendRequestModel + var model = new PushSendRequestModel { - UserId = userId, - OrganizationId = Guid.NewGuid().ToString(), - InstallationId = installationId, + UserId = null, + OrganizationId = Guid.NewGuid(), + InstallationId = null, Type = PushType.SyncCiphers, Payload = "test" }; @@ -70,16 +64,14 @@ public class PushSendRequestModelTests Assert.Empty(results); } - [Theory] - [RepeatingPatternBitAutoData([null, "", " "], [null, "", " "])] - public void Validate_InstallationIdProvidedUserIdOrganizationIdNullOrEmpty_Valid(string? userId, - string? organizationId) + [Fact] + public void Validate_InstallationIdProvidedUserIdOrganizationIdNull_Valid() { - var model = new PushSendRequestModel + var model = new PushSendRequestModel { - UserId = userId, - OrganizationId = organizationId, - InstallationId = Guid.NewGuid().ToString(), + UserId = null, + OrganizationId = null, + InstallationId = Guid.NewGuid(), Type = PushType.SyncCiphers, Payload = "test" }; @@ -94,10 +86,10 @@ public class PushSendRequestModelTests [BitAutoData("Type")] public void Validate_RequiredFieldNotProvided_Invalid(string requiredField) { - var model = new PushSendRequestModel + var model = new PushSendRequestModel { - UserId = Guid.NewGuid().ToString(), - OrganizationId = Guid.NewGuid().ToString(), + UserId = Guid.NewGuid(), + OrganizationId = Guid.NewGuid(), Type = PushType.SyncCiphers, Payload = "test" }; @@ -115,7 +107,7 @@ public class PushSendRequestModelTests var serialized = JsonSerializer.Serialize(dictionary, JsonHelpers.IgnoreWritingNull); var jsonException = - Assert.Throws(() => JsonSerializer.Deserialize(serialized)); + Assert.Throws(() => JsonSerializer.Deserialize>(serialized)); Assert.Contains($"missing required properties, including the following: {requiredField}", jsonException.Message); } @@ -123,15 +115,15 @@ public class PushSendRequestModelTests [Fact] public void Validate_AllFieldsPresent_Valid() { - var model = new PushSendRequestModel + var model = new PushSendRequestModel { - UserId = Guid.NewGuid().ToString(), - OrganizationId = Guid.NewGuid().ToString(), + UserId = Guid.NewGuid(), + OrganizationId = Guid.NewGuid(), Type = PushType.SyncCiphers, Payload = "test payload", Identifier = Guid.NewGuid().ToString(), ClientType = ClientType.All, - DeviceId = Guid.NewGuid().ToString() + DeviceId = Guid.NewGuid() }; var results = Validate(model); @@ -139,7 +131,7 @@ public class PushSendRequestModelTests Assert.Empty(results); } - private static List Validate(PushSendRequestModel model) + private static List Validate(PushSendRequestModel model) { var results = new List(); Validator.TryValidateObject(model, new ValidationContext(model), results, true); diff --git a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs index 6f4ea9ca12..54a6f84339 100644 --- a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs +++ b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs @@ -5,12 +5,11 @@ using Bit.Core.Auth.Entities; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models; -using Bit.Core.Models.Data; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Enums; using Bit.Core.NotificationHub; +using Bit.Core.Platform.Push; using Bit.Core.Repositories; -using Bit.Core.Settings; using Bit.Core.Test.NotificationCenter.AutoFixture; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; @@ -33,483 +32,6 @@ public class NotificationHubPushNotificationServiceTests private static readonly DateTime _now = DateTime.UtcNow; private static readonly Guid _installationId = Guid.Parse("da73177b-513f-4444-b582-595c890e1022"); - [Theory] - [BitAutoData] - [NotificationCustomize] - public async Task PushNotificationAsync_GlobalInstallationIdDefault_NotSent( - SutProvider sutProvider, Notification notification) - { - sutProvider.GetDependency().Installation.Id = default; - - await sutProvider.Sut.PushNotificationAsync(notification); - - await sutProvider.GetDependency() - .Received(0) - .AllClients - .Received(0) - .SendTemplateNotificationAsync(Arg.Any>(), Arg.Any()); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData] - [NotificationCustomize] - public async Task PushNotificationAsync_GlobalInstallationIdSetClientTypeAll_SentToInstallationId( - SutProvider sutProvider, Notification notification, Guid installationId) - { - sutProvider.GetDependency().Installation.Id = installationId; - notification.ClientType = ClientType.All; - var expectedNotification = ToNotificationPushNotification(notification, null, installationId); - - await sutProvider.Sut.PushNotificationAsync(notification); - - await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification, - expectedNotification, - $"(template:payload && installationId:{installationId})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData(ClientType.Browser)] - [BitAutoData(ClientType.Desktop)] - [BitAutoData(ClientType.Web)] - [BitAutoData(ClientType.Mobile)] - [NotificationCustomize] - public async Task PushNotificationAsync_GlobalInstallationIdSetClientTypeNotAll_SentToInstallationIdAndClientType( - ClientType clientType, SutProvider sutProvider, - Notification notification, Guid installationId) - { - sutProvider.GetDependency().Installation.Id = installationId; - notification.ClientType = clientType; - var expectedNotification = ToNotificationPushNotification(notification, null, installationId); - - await sutProvider.Sut.PushNotificationAsync(notification); - - await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification, - expectedNotification, - $"(template:payload && installationId:{installationId} && clientType:{clientType})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData(false)] - [BitAutoData(true)] - [NotificationCustomize(false)] - public async Task PushNotificationAsync_UserIdProvidedClientTypeAll_SentToUser( - bool organizationIdNull, SutProvider sutProvider, - Notification notification) - { - if (organizationIdNull) - { - notification.OrganizationId = null; - } - - notification.ClientType = ClientType.All; - var expectedNotification = ToNotificationPushNotification(notification, null, null); - - await sutProvider.Sut.PushNotificationAsync(notification); - - await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification, - expectedNotification, - $"(template:payload_userId:{notification.UserId})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData(ClientType.Browser)] - [BitAutoData(ClientType.Desktop)] - [BitAutoData(ClientType.Web)] - [BitAutoData(ClientType.Mobile)] - [NotificationCustomize(false)] - public async Task PushNotificationAsync_UserIdProvidedOrganizationIdNullClientTypeNotAll_SentToUser( - ClientType clientType, SutProvider sutProvider, - Notification notification) - { - notification.OrganizationId = null; - notification.ClientType = clientType; - var expectedNotification = ToNotificationPushNotification(notification, null, null); - - await sutProvider.Sut.PushNotificationAsync(notification); - - await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification, - expectedNotification, - $"(template:payload_userId:{notification.UserId} && clientType:{clientType})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData(ClientType.Browser)] - [BitAutoData(ClientType.Desktop)] - [BitAutoData(ClientType.Web)] - [BitAutoData(ClientType.Mobile)] - [NotificationCustomize(false)] - public async Task PushNotificationAsync_UserIdProvidedOrganizationIdProvidedClientTypeNotAll_SentToUser( - ClientType clientType, SutProvider sutProvider, - Notification notification) - { - notification.ClientType = clientType; - var expectedNotification = ToNotificationPushNotification(notification, null, null); - - await sutProvider.Sut.PushNotificationAsync(notification); - - await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification, - expectedNotification, - $"(template:payload_userId:{notification.UserId} && clientType:{clientType})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData] - [NotificationCustomize(false)] - public async Task PushNotificationAsync_UserIdNullOrganizationIdProvidedClientTypeAll_SentToOrganization( - SutProvider sutProvider, Notification notification) - { - notification.UserId = null; - notification.ClientType = ClientType.All; - var expectedNotification = ToNotificationPushNotification(notification, null, null); - - await sutProvider.Sut.PushNotificationAsync(notification); - - await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification, - expectedNotification, - $"(template:payload && organizationId:{notification.OrganizationId})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData(ClientType.Browser)] - [BitAutoData(ClientType.Desktop)] - [BitAutoData(ClientType.Web)] - [BitAutoData(ClientType.Mobile)] - [NotificationCustomize(false)] - public async Task PushNotificationAsync_UserIdNullOrganizationIdProvidedClientTypeNotAll_SentToOrganization( - ClientType clientType, SutProvider sutProvider, - Notification notification) - { - notification.UserId = null; - notification.ClientType = clientType; - var expectedNotification = ToNotificationPushNotification(notification, null, null); - - await sutProvider.Sut.PushNotificationAsync(notification); - - await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification, - expectedNotification, - $"(template:payload && organizationId:{notification.OrganizationId} && clientType:{clientType})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData] - [NotificationCustomize] - public async Task PushNotificationStatusAsync_GlobalInstallationIdDefault_NotSent( - SutProvider sutProvider, Notification notification, - NotificationStatus notificationStatus) - { - sutProvider.GetDependency().Installation.Id = default; - - await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); - - await sutProvider.GetDependency() - .Received(0) - .AllClients - .Received(0) - .SendTemplateNotificationAsync(Arg.Any>(), Arg.Any()); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData] - [NotificationCustomize] - public async Task PushNotificationStatusAsync_GlobalInstallationIdSetClientTypeAll_SentToInstallationId( - SutProvider sutProvider, - Notification notification, NotificationStatus notificationStatus, Guid installationId) - { - sutProvider.GetDependency().Installation.Id = installationId; - notification.ClientType = ClientType.All; - - var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, installationId); - - await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); - - await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus, - expectedNotification, - $"(template:payload && installationId:{installationId})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData(ClientType.Browser)] - [BitAutoData(ClientType.Desktop)] - [BitAutoData(ClientType.Web)] - [BitAutoData(ClientType.Mobile)] - [NotificationCustomize] - public async Task - PushNotificationStatusAsync_GlobalInstallationIdSetClientTypeNotAll_SentToInstallationIdAndClientType( - ClientType clientType, SutProvider sutProvider, - Notification notification, NotificationStatus notificationStatus, Guid installationId) - { - sutProvider.GetDependency().Installation.Id = installationId; - notification.ClientType = clientType; - - var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, installationId); - - await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); - - await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus, - expectedNotification, - $"(template:payload && installationId:{installationId} && clientType:{clientType})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData(false)] - [BitAutoData(true)] - [NotificationCustomize(false)] - public async Task PushNotificationStatusAsync_UserIdProvidedClientTypeAll_SentToUser( - bool organizationIdNull, SutProvider sutProvider, - Notification notification, NotificationStatus notificationStatus) - { - if (organizationIdNull) - { - notification.OrganizationId = null; - } - - notification.ClientType = ClientType.All; - var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null); - - await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); - - await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus, - expectedNotification, - $"(template:payload_userId:{notification.UserId})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData(ClientType.Browser)] - [BitAutoData(ClientType.Desktop)] - [BitAutoData(ClientType.Web)] - [BitAutoData(ClientType.Mobile)] - [NotificationCustomize(false)] - public async Task PushNotificationStatusAsync_UserIdProvidedOrganizationIdNullClientTypeNotAll_SentToUser( - ClientType clientType, SutProvider sutProvider, - Notification notification, NotificationStatus notificationStatus) - { - notification.OrganizationId = null; - notification.ClientType = clientType; - var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null); - - await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); - - await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus, - expectedNotification, - $"(template:payload_userId:{notification.UserId} && clientType:{clientType})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData(ClientType.Browser)] - [BitAutoData(ClientType.Desktop)] - [BitAutoData(ClientType.Web)] - [BitAutoData(ClientType.Mobile)] - [NotificationCustomize(false)] - public async Task PushNotificationStatusAsync_UserIdProvidedOrganizationIdProvidedClientTypeNotAll_SentToUser( - ClientType clientType, SutProvider sutProvider, - Notification notification, NotificationStatus notificationStatus) - { - notification.ClientType = clientType; - var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null); - - await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); - - await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus, - expectedNotification, - $"(template:payload_userId:{notification.UserId} && clientType:{clientType})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData] - [NotificationCustomize(false)] - public async Task PushNotificationStatusAsync_UserIdNullOrganizationIdProvidedClientTypeAll_SentToOrganization( - SutProvider sutProvider, Notification notification, - NotificationStatus notificationStatus) - { - notification.UserId = null; - notification.ClientType = ClientType.All; - var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null); - - await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); - - await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus, - expectedNotification, - $"(template:payload && organizationId:{notification.OrganizationId})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData(ClientType.Browser)] - [BitAutoData(ClientType.Desktop)] - [BitAutoData(ClientType.Web)] - [BitAutoData(ClientType.Mobile)] - [NotificationCustomize(false)] - public async Task - PushNotificationStatusAsync_UserIdNullOrganizationIdProvidedClientTypeNotAll_SentToOrganization( - ClientType clientType, SutProvider sutProvider, - Notification notification, NotificationStatus notificationStatus) - { - notification.UserId = null; - notification.ClientType = clientType; - var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null); - - await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); - - await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus, - expectedNotification, - $"(template:payload && organizationId:{notification.OrganizationId} && clientType:{clientType})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData([null])] - [BitAutoData(ClientType.All)] - public async Task SendPayloadToUserAsync_ClientTypeNullOrAll_SentToUser(ClientType? clientType, - SutProvider sutProvider, Guid userId, PushType pushType, string payload, - string identifier) - { - await sutProvider.Sut.SendPayloadToUserAsync(userId.ToString(), pushType, payload, identifier, null, - clientType); - - await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload, - $"(template:payload_userId:{userId} && !deviceIdentifier:{identifier})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData(ClientType.Browser)] - [BitAutoData(ClientType.Desktop)] - [BitAutoData(ClientType.Mobile)] - [BitAutoData(ClientType.Web)] - public async Task SendPayloadToUserAsync_ClientTypeExplicit_SentToUserAndClientType(ClientType clientType, - SutProvider sutProvider, Guid userId, PushType pushType, string payload, - string identifier) - { - await sutProvider.Sut.SendPayloadToUserAsync(userId.ToString(), pushType, payload, identifier, null, - clientType); - - await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload, - $"(template:payload_userId:{userId} && !deviceIdentifier:{identifier} && clientType:{clientType})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData([null])] - [BitAutoData(ClientType.All)] - public async Task SendPayloadToOrganizationAsync_ClientTypeNullOrAll_SentToOrganization(ClientType? clientType, - SutProvider sutProvider, Guid organizationId, PushType pushType, - string payload, string identifier) - { - await sutProvider.Sut.SendPayloadToOrganizationAsync(organizationId.ToString(), pushType, payload, identifier, - null, clientType); - - await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload, - $"(template:payload && organizationId:{organizationId} && !deviceIdentifier:{identifier})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData(ClientType.Browser)] - [BitAutoData(ClientType.Desktop)] - [BitAutoData(ClientType.Mobile)] - [BitAutoData(ClientType.Web)] - public async Task SendPayloadToOrganizationAsync_ClientTypeExplicit_SentToOrganizationAndClientType( - ClientType clientType, SutProvider sutProvider, Guid organizationId, - PushType pushType, string payload, string identifier) - { - await sutProvider.Sut.SendPayloadToOrganizationAsync(organizationId.ToString(), pushType, payload, identifier, - null, clientType); - - await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload, - $"(template:payload && organizationId:{organizationId} && !deviceIdentifier:{identifier} && clientType:{clientType})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData([null])] - [BitAutoData(ClientType.All)] - public async Task SendPayloadToInstallationAsync_ClientTypeNullOrAll_SentToInstallation(ClientType? clientType, - SutProvider sutProvider, Guid installationId, PushType pushType, - string payload, string identifier) - { - await sutProvider.Sut.SendPayloadToInstallationAsync(installationId.ToString(), pushType, payload, identifier, - null, clientType); - - await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload, - $"(template:payload && installationId:{installationId} && !deviceIdentifier:{identifier})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - - [Theory] - [BitAutoData(ClientType.Browser)] - [BitAutoData(ClientType.Desktop)] - [BitAutoData(ClientType.Mobile)] - [BitAutoData(ClientType.Web)] - public async Task SendPayloadToInstallationAsync_ClientTypeExplicit_SentToInstallationAndClientType( - ClientType clientType, SutProvider sutProvider, Guid installationId, - PushType pushType, string payload, string identifier) - { - await sutProvider.Sut.SendPayloadToInstallationAsync(installationId.ToString(), pushType, payload, identifier, - null, clientType); - - await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload, - $"(template:payload && installationId:{installationId} && !deviceIdentifier:{identifier} && clientType:{clientType})"); - await sutProvider.GetDependency() - .Received(0) - .UpsertAsync(Arg.Any()); - } - [Fact] public async Task PushSyncCipherCreateAsync_SendExpectedData() { @@ -1066,7 +588,7 @@ public class NotificationHubPushNotificationServiceTests ); } - private async Task VerifyNotificationAsync(Func test, + private async Task VerifyNotificationAsync(Func test, PushType type, JsonNode expectedPayload, string tag) { var installationDeviceRepository = Substitute.For(); @@ -1104,12 +626,11 @@ public class NotificationHubPushNotificationServiceTests notificationHubPool, httpContextAccessor, NullLogger.Instance, - globalSettings, - fakeTimeProvider + globalSettings ); // Act - await test(sut); + await test(new EngineWrapper(sut, fakeTimeProvider, _installationId)); // Assert var calls = notificationHubProxy.ReceivedCalls(); diff --git a/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs index e57544b48a..4e2ec19086 100644 --- a/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs @@ -9,14 +9,11 @@ using Bit.Core.Enums; using Bit.Core.Models; 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.Test.AutoFixture; -using Bit.Core.Test.AutoFixture.CurrentContextFixtures; -using Bit.Core.Test.NotificationCenter.AutoFixture; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; -using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -42,96 +39,6 @@ public class AzureQueuePushNotificationServiceTests _fakeTimeProvider.SetUtcNow(DateTime.UtcNow); } - [Theory] - [BitAutoData] - [NotificationCustomize] - [CurrentContextCustomize] - public async Task PushNotificationAsync_NotificationGlobal_Sent( - SutProvider sutProvider, Notification notification, Guid deviceIdentifier, - ICurrentContext currentContext, Guid installationId) - { - currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); - sutProvider.GetDependency().HttpContext!.RequestServices - .GetService(Arg.Any()).Returns(currentContext); - sutProvider.GetDependency().Installation.Id = installationId; - - await sutProvider.Sut.PushNotificationAsync(notification); - - await sutProvider.GetDependency().Received(1) - .SendMessageAsync(Arg.Is(message => - MatchMessage(PushType.Notification, message, - new NotificationPushNotificationEquals(notification, null, installationId), - deviceIdentifier.ToString()))); - } - - [Theory] - [BitAutoData] - [NotificationCustomize(false)] - [CurrentContextCustomize] - public async Task PushNotificationAsync_NotificationNotGlobal_Sent( - SutProvider sutProvider, Notification notification, Guid deviceIdentifier, - ICurrentContext currentContext, Guid installationId) - { - currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); - sutProvider.GetDependency().HttpContext!.RequestServices - .GetService(Arg.Any()).Returns(currentContext); - sutProvider.GetDependency().Installation.Id = installationId; - - await sutProvider.Sut.PushNotificationAsync(notification); - - await sutProvider.GetDependency().Received(1) - .SendMessageAsync(Arg.Is(message => - MatchMessage(PushType.Notification, message, - new NotificationPushNotificationEquals(notification, null, null), - deviceIdentifier.ToString()))); - } - - [Theory] - [BitAutoData] - [NotificationCustomize] - [NotificationStatusCustomize] - [CurrentContextCustomize] - public async Task PushNotificationStatusAsync_NotificationGlobal_Sent( - SutProvider sutProvider, Notification notification, Guid deviceIdentifier, - ICurrentContext currentContext, NotificationStatus notificationStatus, Guid installationId) - { - currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); - sutProvider.GetDependency().HttpContext!.RequestServices - .GetService(Arg.Any()).Returns(currentContext); - sutProvider.GetDependency().Installation.Id = installationId; - - await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); - - await sutProvider.GetDependency().Received(1) - .SendMessageAsync(Arg.Is(message => - MatchMessage(PushType.NotificationStatus, message, - new NotificationPushNotificationEquals(notification, notificationStatus, installationId), - deviceIdentifier.ToString()))); - } - - [Theory] - [BitAutoData] - [NotificationCustomize(false)] - [NotificationStatusCustomize] - [CurrentContextCustomize] - public async Task PushNotificationStatusAsync_NotificationNotGlobal_Sent( - SutProvider sutProvider, Notification notification, Guid deviceIdentifier, - ICurrentContext currentContext, NotificationStatus notificationStatus, Guid installationId) - { - currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); - sutProvider.GetDependency().HttpContext!.RequestServices - .GetService(Arg.Any()).Returns(currentContext); - sutProvider.GetDependency().Installation.Id = installationId; - - await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); - - await sutProvider.GetDependency().Received(1) - .SendMessageAsync(Arg.Is(message => - MatchMessage(PushType.NotificationStatus, message, - new NotificationPushNotificationEquals(notification, notificationStatus, null), - deviceIdentifier.ToString()))); - } - [Theory] [InlineData("6a5bbe1b-cf16-49a6-965f-5c2eac56a531", null)] [InlineData(null, "b9a3fcb4-2447-45c1-aad2-24de43c88c44")] @@ -844,7 +751,7 @@ public class AzureQueuePushNotificationServiceTests // ); // } - private async Task VerifyNotificationAsync(Func test, JsonNode expectedMessage) + private async Task VerifyNotificationAsync(Func test, JsonNode expectedMessage) { var queueClient = Substitute.For(); @@ -872,7 +779,7 @@ public class AzureQueuePushNotificationServiceTests _fakeTimeProvider ); - await test(sut); + await test(new EngineWrapper(sut, _fakeTimeProvider, _globalSettings.Installation.Id)); // Hoist equality checker outside the expression so that we // can more easily place a breakpoint diff --git a/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs index 68acf7ec72..a1bc2c6547 100644 --- a/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs @@ -1,98 +1,8 @@ #nullable enable -using Bit.Core.Enums; -using Bit.Core.NotificationCenter.Entities; -using Bit.Core.Platform.Push; -using Bit.Core.Platform.Push.Internal; -using Bit.Core.Test.NotificationCenter.AutoFixture; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using NSubstitute; -using Xunit; namespace Bit.Core.Test.Platform.Push.Services; -[SutProviderCustomize] public class MultiServicePushNotificationServiceTests { - [Theory] - [BitAutoData] - [NotificationCustomize] - public async Task PushNotificationAsync_Notification_Sent( - SutProvider sutProvider, Notification notification) - { - await sutProvider.Sut.PushNotificationAsync(notification); - - await sutProvider.GetDependency>() - .First() - .Received(1) - .PushNotificationAsync(notification); - } - - [Theory] - [BitAutoData] - [NotificationCustomize] - [NotificationStatusCustomize] - public async Task PushNotificationStatusAsync_Notification_Sent( - SutProvider sutProvider, Notification notification, - NotificationStatus notificationStatus) - { - await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); - - await sutProvider.GetDependency>() - .First() - .Received(1) - .PushNotificationStatusAsync(notification, notificationStatus); - } - - [Theory] - [BitAutoData([null, null])] - [BitAutoData(ClientType.All, null)] - [BitAutoData([null, "test device id"])] - [BitAutoData(ClientType.All, "test device id")] - public async Task SendPayloadToUserAsync_Message_Sent(ClientType? clientType, string? deviceId, string userId, - PushType type, object payload, string identifier, SutProvider sutProvider) - { - await sutProvider.Sut.SendPayloadToUserAsync(userId, type, payload, identifier, deviceId, clientType); - - await sutProvider.GetDependency>() - .First() - .Received(1) - .SendPayloadToUserAsync(userId, type, payload, identifier, deviceId, clientType); - } - - [Theory] - [BitAutoData([null, null])] - [BitAutoData(ClientType.All, null)] - [BitAutoData([null, "test device id"])] - [BitAutoData(ClientType.All, "test device id")] - public async Task SendPayloadToOrganizationAsync_Message_Sent(ClientType? clientType, string? deviceId, - string organizationId, PushType type, object payload, string identifier, - SutProvider sutProvider) - { - await sutProvider.Sut.SendPayloadToOrganizationAsync(organizationId, type, payload, identifier, deviceId, - clientType); - - await sutProvider.GetDependency>() - .First() - .Received(1) - .SendPayloadToOrganizationAsync(organizationId, type, payload, identifier, deviceId, clientType); - } - - [Theory] - [BitAutoData([null, null])] - [BitAutoData(ClientType.All, null)] - [BitAutoData([null, "test device id"])] - [BitAutoData(ClientType.All, "test device id")] - public async Task SendPayloadToInstallationAsync_Message_Sent(ClientType? clientType, string? deviceId, - string installationId, PushType type, object payload, string identifier, - SutProvider sutProvider) - { - await sutProvider.Sut.SendPayloadToInstallationAsync(installationId, type, payload, identifier, deviceId, - clientType); - - await sutProvider.GetDependency>() - .First() - .Received(1) - .SendPayloadToInstallationAsync(installationId, type, payload, identifier, deviceId, clientType); - } + // TODO: Can add a couple tests here } diff --git a/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs index d206d96d44..92706c6ccc 100644 --- a/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs @@ -19,14 +19,13 @@ public class NotificationsApiPushNotificationServiceTests : PushTestBase protected override string ExpectedClientUrl() => "https://localhost:7777/send"; - protected override IPushNotificationService CreateService() + protected override IPushEngine CreateService() { return new NotificationsApiPushNotificationService( HttpClientFactory, GlobalSettings, HttpContextAccessor, - NullLogger.Instance, - FakeTimeProvider + NullLogger.Instance ); } @@ -221,7 +220,7 @@ public class NotificationsApiPushNotificationServiceTests : PushTestBase ["UserId"] = send.UserId, ["RevisionDate"] = send.RevisionDate, }, - ["ContextId"] = null, + ["ContextId"] = DeviceIdentifier, }; } @@ -236,7 +235,7 @@ public class NotificationsApiPushNotificationServiceTests : PushTestBase ["UserId"] = send.UserId, ["RevisionDate"] = send.RevisionDate, }, - ["ContextId"] = null, + ["ContextId"] = DeviceIdentifier, }; } @@ -251,7 +250,7 @@ public class NotificationsApiPushNotificationServiceTests : PushTestBase ["UserId"] = send.UserId, ["RevisionDate"] = send.RevisionDate, }, - ["ContextId"] = null, + ["ContextId"] = DeviceIdentifier, }; } diff --git a/test/Core.Test/Platform/Push/Services/PushTestBase.cs b/test/Core.Test/Platform/Push/Services/PushTestBase.cs index 111df7ca26..3538a68127 100644 --- a/test/Core.Test/Platform/Push/Services/PushTestBase.cs +++ b/test/Core.Test/Platform/Push/Services/PushTestBase.cs @@ -15,11 +15,28 @@ using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; using NSubstitute; using RichardSzalay.MockHttp; using Xunit; +public class EngineWrapper(IPushEngine pushEngine, FakeTimeProvider fakeTimeProvider, Guid installationId) : IPushNotificationService +{ + public Guid InstallationId { get; } = installationId; + + public TimeProvider TimeProvider { get; } = fakeTimeProvider; + + public ILogger Logger => NullLogger.Instance; + + public Task PushAsync(PushNotification pushNotification) where T : class + => pushEngine.PushAsync(pushNotification); + + public Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable? collectionIds) + => pushEngine.PushCipherAsync(cipher, pushType, collectionIds); +} + public abstract class PushTestBase { protected static readonly string DeviceIdentifier = "test_device_identifier"; @@ -51,7 +68,7 @@ public abstract class PushTestBase FakeTimeProvider.SetUtcNow(DateTimeOffset.UtcNow); } - protected abstract IPushNotificationService CreateService(); + protected abstract IPushEngine CreateService(); protected abstract string ExpectedClientUrl(); @@ -480,7 +497,7 @@ public abstract class PushTestBase }) .Respond(HttpStatusCode.OK); - await test(CreateService()); + await test(new EngineWrapper(CreateService(), FakeTimeProvider, GlobalSettings.Installation.Id)); Assert.NotNull(actualNode); diff --git a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs index faa6b5dfa7..f95531c944 100644 --- a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs @@ -4,8 +4,8 @@ using System.Text.Json.Nodes; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Entities; -using Bit.Core.Enums; using Bit.Core.NotificationCenter.Entities; +using Bit.Core.Platform.Push; using Bit.Core.Platform.Push.Internal; using Bit.Core.Repositories; using Bit.Core.Settings; @@ -14,7 +14,6 @@ using Bit.Core.Vault.Entities; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; using NSubstitute; -using Xunit; namespace Bit.Core.Test.Platform.Push.Services; @@ -38,47 +37,19 @@ public class RelayPushNotificationServiceTests : PushTestBase GlobalSettings.Installation.IdentityUri = "https://localhost:8888"; } - protected override RelayPushNotificationService CreateService() + protected override IPushEngine CreateService() { return new RelayPushNotificationService( HttpClientFactory, _deviceRepository, GlobalSettings, HttpContextAccessor, - NullLogger.Instance, - FakeTimeProvider + NullLogger.Instance ); } protected override string ExpectedClientUrl() => "https://localhost:7777/push/send"; - [Fact] - public async Task SendPayloadToInstallationAsync_ThrowsNotImplementedException() - { - var sut = CreateService(); - await Assert.ThrowsAsync( - async () => await sut.SendPayloadToInstallationAsync("installation_id", PushType.AuthRequest, new { }, null) - ); - } - - [Fact] - public async Task SendPayloadToUserAsync_ThrowsNotImplementedException() - { - var sut = CreateService(); - await Assert.ThrowsAsync( - async () => await sut.SendPayloadToUserAsync("user_id", PushType.AuthRequest, new { }, null) - ); - } - - [Fact] - public async Task SendPayloadToOrganizationAsync_ThrowsNotImplementedException() - { - var sut = CreateService(); - await Assert.ThrowsAsync( - async () => await sut.SendPayloadToOrganizationAsync("organization_id", PushType.AuthRequest, new { }, null) - ); - } - protected override JsonNode GetPushSyncCipherCreatePayload(Cipher cipher, Guid collectionIds) { return new JsonObject From 502ab4b6451ff1f115af209254967dd46a88e517 Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Wed, 18 Jun 2025 10:09:47 -0400 Subject: [PATCH 11/47] [PM-17562] Fix flickering unit test - WebhookIntegrationHandlerTests (#5973) * [PM-17562] Fix flickering unit test - WebhookIntegrationHandlerTests * Adjust to using TimeProvider and exact time matches * Refactored RabittMqIntegrationListenerService and Tests to align on TimeProvider. Cleaned up tests that do not need to use DateTime.UtcNow --- .../RabbitMqIntegrationListenerService.cs | 7 +++++-- .../WebhookIntegrationHandler.cs | 6 ++++-- .../Utilities/ServiceCollectionExtensions.cs | 3 ++- ...rviceBusIntegrationListenerServiceTests.cs | 5 ----- ...RabbitMqIntegrationListenerServiceTests.cs | 20 ++++++++++++------- .../WebhookIntegrationHandlerTests.cs | 18 ++++++++++++----- 6 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs index a60738d62d..db6a7f9510 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs @@ -20,6 +20,7 @@ public class RabbitMqIntegrationListenerService : BackgroundService private readonly Lazy> _lazyChannel; private readonly IRabbitMqService _rabbitMqService; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public RabbitMqIntegrationListenerService(IIntegrationHandler handler, string routingKey, @@ -27,7 +28,8 @@ public class RabbitMqIntegrationListenerService : BackgroundService string retryQueueName, int maxRetries, IRabbitMqService rabbitMqService, - ILogger logger) + ILogger logger, + TimeProvider timeProvider) { _handler = handler; _routingKey = routingKey; @@ -35,6 +37,7 @@ public class RabbitMqIntegrationListenerService : BackgroundService _queueName = queueName; _rabbitMqService = rabbitMqService; _logger = logger; + _timeProvider = timeProvider; _maxRetries = maxRetries; _lazyChannel = new Lazy>(() => _rabbitMqService.CreateChannelAsync()); } @@ -74,7 +77,7 @@ public class RabbitMqIntegrationListenerService : BackgroundService var integrationMessage = JsonSerializer.Deserialize(json); if (integrationMessage is not null && integrationMessage.DelayUntilDate.HasValue && - integrationMessage.DelayUntilDate.Value > DateTime.UtcNow) + integrationMessage.DelayUntilDate.Value > _timeProvider.GetUtcNow().UtcDateTime) { await _rabbitMqService.RepublishToRetryQueueAsync(channel, ea); await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs index 3d76077483..6dc348310d 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs @@ -9,7 +9,9 @@ using Bit.Core.AdminConsole.Models.Data.EventIntegrations; namespace Bit.Core.Services; -public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory) +public class WebhookIntegrationHandler( + IHttpClientFactory httpClientFactory, + TimeProvider timeProvider) : IntegrationHandlerBase { private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName); @@ -39,7 +41,7 @@ public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory) if (int.TryParse(value, out var seconds)) { // Retry-after was specified in seconds. Adjust DelayUntilDate by the requested number of seconds. - result.DelayUntilDate = DateTime.UtcNow.AddSeconds(seconds); + result.DelayUntilDate = timeProvider.GetUtcNow().AddSeconds(seconds).UtcDateTime; } else if (DateTimeOffset.TryParseExact(value, "r", // "r" is the round-trip format: RFC1123 diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 871a1be038..83015354bb 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -717,7 +717,8 @@ public static class ServiceCollectionExtensions retryQueueName: integrationRetryQueueName, maxRetries: maxRetries, rabbitMqService: provider.GetRequiredService(), - logger: provider.GetRequiredService>())); + logger: provider.GetRequiredService>(), + timeProvider: provider.GetRequiredService())); return services; } diff --git a/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs index f53df626d1..32a305266d 100644 --- a/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs @@ -52,7 +52,6 @@ public class AzureServiceBusIntegrationListenerServiceTests public async Task HandleMessageAsync_FailureNotRetryable_PublishesToDeadLetterQueue(IntegrationMessage message) { var sutProvider = GetSutProvider(); - message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); message.RetryCount = 0; var result = new IntegrationHandlerResult(false, message); @@ -71,7 +70,6 @@ public class AzureServiceBusIntegrationListenerServiceTests public async Task HandleMessageAsync_FailureRetryableButTooManyRetries_PublishesToDeadLetterQueue(IntegrationMessage message) { var sutProvider = GetSutProvider(); - message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); message.RetryCount = _maxRetries; var result = new IntegrationHandlerResult(false, message); result.Retryable = true; @@ -90,12 +88,10 @@ public class AzureServiceBusIntegrationListenerServiceTests public async Task HandleMessageAsync_FailureRetryable_PublishesToRetryQueue(IntegrationMessage message) { var sutProvider = GetSutProvider(); - message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); message.RetryCount = 0; var result = new IntegrationHandlerResult(false, message); result.Retryable = true; - result.DelayUntilDate = DateTime.UtcNow.AddMinutes(1); _handler.HandleAsync(Arg.Any()).Returns(result); var expected = (IntegrationMessage)IntegrationMessage.FromJson(message.ToJson())!; @@ -110,7 +106,6 @@ public class AzureServiceBusIntegrationListenerServiceTests public async Task HandleMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage message) { var sutProvider = GetSutProvider(); - message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); var result = new IntegrationHandlerResult(true, message); _handler.HandleAsync(Arg.Any()).Returns(result); diff --git a/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs index da0b8ec377..bb3f211afa 100644 --- a/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs @@ -4,6 +4,7 @@ using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; +using Microsoft.Extensions.Time.Testing; using NSubstitute; using RabbitMQ.Client; using RabbitMQ.Client.Events; @@ -18,19 +19,24 @@ public class RabbitMqIntegrationListenerServiceTests 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 SutProvider GetSutProvider() { - return new SutProvider() + var sutProvider = new SutProvider() .SetDependency(_handler) .SetDependency(_rabbitMqService) .SetDependency(_queueName, "queueName") .SetDependency(_retryQueueName, "retryQueueName") .SetDependency(_routingKey, "routingKey") .SetDependency(_maxRetries, "maxRetries") + .WithFakeTimeProvider() .Create(); + sutProvider.GetDependency().SetUtcNow(_now); + + return sutProvider; } [Fact] @@ -55,7 +61,7 @@ public class RabbitMqIntegrationListenerServiceTests var cancellationToken = CancellationToken.None; await sutProvider.Sut.StartAsync(cancellationToken); - message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + message.DelayUntilDate = null; message.RetryCount = 0; var eventArgs = new BasicDeliverEventArgs( consumerTag: string.Empty, @@ -94,7 +100,7 @@ public class RabbitMqIntegrationListenerServiceTests var cancellationToken = CancellationToken.None; await sutProvider.Sut.StartAsync(cancellationToken); - message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + message.DelayUntilDate = null; message.RetryCount = _maxRetries; var eventArgs = new BasicDeliverEventArgs( consumerTag: string.Empty, @@ -132,7 +138,7 @@ public class RabbitMqIntegrationListenerServiceTests var cancellationToken = CancellationToken.None; await sutProvider.Sut.StartAsync(cancellationToken); - message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + message.DelayUntilDate = null; message.RetryCount = 0; var eventArgs = new BasicDeliverEventArgs( consumerTag: string.Empty, @@ -145,7 +151,7 @@ public class RabbitMqIntegrationListenerServiceTests ); var result = new IntegrationHandlerResult(false, message); result.Retryable = true; - result.DelayUntilDate = DateTime.UtcNow.AddMinutes(1); + result.DelayUntilDate = _now.AddMinutes(1); _handler.HandleAsync(Arg.Any()).Returns(result); var expected = IntegrationMessage.FromJson(message.ToJson()); @@ -173,7 +179,7 @@ public class RabbitMqIntegrationListenerServiceTests var cancellationToken = CancellationToken.None; await sutProvider.Sut.StartAsync(cancellationToken); - message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + message.DelayUntilDate = null; var eventArgs = new BasicDeliverEventArgs( consumerTag: string.Empty, deliveryTag: 0, @@ -205,7 +211,7 @@ public class RabbitMqIntegrationListenerServiceTests var cancellationToken = CancellationToken.None; await sutProvider.Sut.StartAsync(cancellationToken); - message.DelayUntilDate = DateTime.UtcNow.AddMinutes(1); + message.DelayUntilDate = _now.AddMinutes(1); var eventArgs = new BasicDeliverEventArgs( consumerTag: string.Empty, deliveryTag: 0, diff --git a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs index 3461b1b607..676a975b77 100644 --- a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs @@ -5,6 +5,7 @@ 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; @@ -33,6 +34,7 @@ public class WebhookIntegrationHandlerTests return new SutProvider() .SetDependency(clientFactory) + .WithFakeTimeProvider() .Create(); } @@ -62,9 +64,13 @@ public class WebhookIntegrationHandlerTests } [Theory, BitAutoData] - public async Task HandleAsync_TooManyRequests_ReturnsFailureSetsNotBeforUtc(IntegrationMessage message) + 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 WebhookIntegrationConfigurationDetails(_webhookUrl); _handler.Fallback @@ -78,19 +84,21 @@ public class WebhookIntegrationHandlerTests Assert.True(result.Retryable); Assert.Equal(result.Message, message); Assert.True(result.DelayUntilDate.HasValue); - Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61)); + Assert.Equal(retryAfter, result.DelayUntilDate.Value); Assert.Equal("Too Many Requests", result.FailureReason); } [Theory, BitAutoData] - public async Task HandleAsync_TooManyRequestsWithDate_ReturnsFailureSetsNotBeforUtc(IntegrationMessage message) + 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 WebhookIntegrationConfigurationDetails(_webhookUrl); _handler.Fallback .WithStatusCode(HttpStatusCode.TooManyRequests) - .WithHeader("Retry-After", DateTime.UtcNow.AddSeconds(60).ToString("r")) // "r" is the round-trip format: RFC1123 + .WithHeader("Retry-After", retryAfter.ToString("r")) .WithContent(new StringContent("testtest")); var result = await sutProvider.Sut.HandleAsync(message); @@ -99,7 +107,7 @@ public class WebhookIntegrationHandlerTests Assert.True(result.Retryable); Assert.Equal(result.Message, message); Assert.True(result.DelayUntilDate.HasValue); - Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61)); + Assert.Equal(retryAfter, result.DelayUntilDate.Value); Assert.Equal("Too Many Requests", result.FailureReason); } From 05d74754d2ec5fae1c683a09f033c228cd914769 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 18 Jun 2025 09:29:48 -0500 Subject: [PATCH 12/47] add `PM22134SdkCipherListView` feature flag (#5980) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 40a0b8a1b0..4ca9ae4702 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -206,6 +206,7 @@ public static class FeatureFlagKeys public const string EndUserNotifications = "pm-10609-end-user-notifications"; public const string PhishingDetection = "phishing-detection"; public const string RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy"; + public const string PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view"; public static List GetAllKeys() { From 91b4ef756b8518edce9aac04913a3c24a552b6ec Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Wed, 18 Jun 2025 10:47:30 -0400 Subject: [PATCH 13/47] build(ci): remove the need to cherry pick version bumps to rc (#5977) --- .github/workflows/repository-management.yml | 125 ++++---------------- 1 file changed, 20 insertions(+), 105 deletions(-) diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index 178e29212a..a59bbcfa6c 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -22,6 +22,8 @@ on: required: false type: string +permissions: {} + jobs: setup: name: Setup @@ -44,49 +46,11 @@ jobs: echo "branch=$BRANCH" >> $GITHUB_OUTPUT - - cut_branch: - name: Cut branch - if: ${{ needs.setup.outputs.branch != 'none' }} - needs: setup - runs-on: ubuntu-24.04 - steps: - - 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 }} - - - name: Check out target ref - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - ref: ${{ inputs.target_ref }} - token: ${{ steps.app-token.outputs.token }} - - - name: Check if ${{ needs.setup.outputs.branch }} branch exists - env: - BRANCH_NAME: ${{ needs.setup.outputs.branch }} - run: | - if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then - echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - - name: Cut branch - env: - BRANCH_NAME: ${{ needs.setup.outputs.branch }} - run: | - git switch --quiet --create $BRANCH_NAME - git push --quiet --set-upstream origin $BRANCH_NAME - - bump_version: name: Bump Version if: ${{ always() }} runs-on: ubuntu-24.04 needs: - - cut_branch - setup outputs: version: ${{ steps.set-final-version-output.outputs.version }} @@ -187,14 +151,13 @@ jobs: - name: Push changes run: git push - - cherry_pick: - name: Cherry-Pick Commit(s) + cut_branch: + name: Cut branch if: ${{ needs.setup.outputs.branch != 'none' }} - runs-on: ubuntu-24.04 needs: - - bump_version - setup + - bump_version + runs-on: ubuntu-24.04 steps: - name: Generate GH App token uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1 @@ -203,78 +166,30 @@ jobs: app-id: ${{ secrets.BW_GHAPP_ID }} private-key: ${{ secrets.BW_GHAPP_KEY }} - - name: Check out main branch + - name: Check out target ref uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - fetch-depth: 0 - ref: main + ref: ${{ inputs.target_ref }} token: ${{ steps.app-token.outputs.token }} - - name: Configure Git - run: | - git config --local user.email "actions@github.com" - git config --local user.name "Github Actions" - - - name: Install xmllint - run: | - sudo apt-get update - sudo apt-get install -y libxml2-utils - - - name: Perform cherry-pick(s) + - name: Check if ${{ needs.setup.outputs.branch }} branch exists env: - CUT_BRANCH: ${{ needs.setup.outputs.branch }} + BRANCH_NAME: ${{ needs.setup.outputs.branch }} run: | - # Function for cherry-picking - cherry_pick () { - local source_branch=$1 - local destination_branch=$2 - - # Get project commit/version from source branch - git switch $source_branch - SOURCE_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 Directory.Build.props) - SOURCE_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props) - - # Get project commit/version from destination branch - git switch $destination_branch - DESTINATION_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props) - - if [[ "$DESTINATION_VERSION" != "$SOURCE_VERSION" ]]; then - git cherry-pick --strategy-option=theirs -x $SOURCE_COMMIT - git push -u origin $destination_branch - fi - } - - # If we are cutting 'hotfix-rc': - if [[ "$CUT_BRANCH" == "hotfix-rc" ]]; then - - # If the 'rc' branch exists: - if [[ $(git ls-remote --heads origin rc) ]]; then - - # Chery-pick from 'rc' into 'hotfix-rc' - cherry_pick rc hotfix-rc - - # Cherry-pick from 'main' into 'rc' - cherry_pick main rc - - # If the 'rc' branch does not exist: - else - - # Cherry-pick from 'main' into 'hotfix-rc' - cherry_pick main hotfix-rc - - fi - - # If we are cutting 'rc': - elif [[ "$CUT_BRANCH" == "rc" ]]; then - - # Cherry-pick from 'main' into 'rc' - cherry_pick main rc - + if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then + echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY + exit 1 fi + - name: Cut branch + env: + BRANCH_NAME: ${{ needs.setup.outputs.branch }} + run: | + git switch --quiet --create $BRANCH_NAME + git push --quiet --set-upstream origin $BRANCH_NAME move_future_db_scripts: name: Move finalization database scripts - needs: cherry_pick + needs: cut_branch uses: ./.github/workflows/_move_finalization_db_scripts.yml secrets: inherit From b13c95032800b76587e43c31d89d7963c6edc0cd Mon Sep 17 00:00:00 2001 From: Andy Pixley <3723676+pixman20@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:15:38 -0400 Subject: [PATCH 14/47] [BRE-848] Adding Workflow Permissions (#5985) --- .github/workflows/enforce-labels.yml | 3 +++ .github/workflows/protect-files.yml | 3 +++ .github/workflows/stale-bot.yml | 5 +++++ .github/workflows/test-database.yml | 7 +++++++ 4 files changed, 18 insertions(+) diff --git a/.github/workflows/enforce-labels.yml b/.github/workflows/enforce-labels.yml index 11d5654937..353127c751 100644 --- a/.github/workflows/enforce-labels.yml +++ b/.github/workflows/enforce-labels.yml @@ -4,6 +4,9 @@ on: workflow_call: pull_request: types: [labeled, unlabeled, opened, reopened, synchronize] + +permissions: {} + jobs: enforce-label: if: ${{ contains(github.event.*.labels.*.name, 'hold') || contains(github.event.*.labels.*.name, 'needs-qa') || contains(github.event.*.labels.*.name, 'DB-migrations-changed') || contains(github.event.*.labels.*.name, 'ephemeral-environment') }} diff --git a/.github/workflows/protect-files.yml b/.github/workflows/protect-files.yml index 89d6d4c6d9..546b8344a6 100644 --- a/.github/workflows/protect-files.yml +++ b/.github/workflows/protect-files.yml @@ -16,6 +16,9 @@ jobs: changed-files: name: Check for file changes runs-on: ubuntu-22.04 + permissions: + contents: read + pull-requests: write outputs: changes: ${{steps.check-changes.outputs.changes_detected}} diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index 9420f71cb3..83d492645e 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -8,6 +8,11 @@ jobs: stale: name: Check for stale issues and PRs runs-on: ubuntu-22.04 + permissions: + actions: write + contents: read + issues: write + pull-requests: write steps: - name: Check uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index 26db5ea0a4..23722e2e8d 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -31,10 +31,17 @@ on: - "test/Infrastructure.IntegrationTest/**" # Any changes to the tests - "src/**/Entities/**/*.cs" # Database entity definitions +permissions: + contents: read + jobs: test: name: Run tests runs-on: ubuntu-22.04 + permissions: + contents: read + actions: read + checks: write steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 From cdfe51f9d63214f44db4583248e770bfdb0df999 Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Fri, 20 Jun 2025 14:02:48 -0400 Subject: [PATCH 15/47] [PM-22783] Add windows-desktop-autotype feature flag (#5990) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 4ca9ae4702..0bd8baa85e 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -138,6 +138,7 @@ public static class FeatureFlagKeys public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill"; public const string MacOsNativeCredentialSync = "macos-native-credential-sync"; public const string InlineMenuTotp = "inline-menu-totp"; + public const string WindowsDesktopAutotype = "windows-desktop-autotype"; /* Billing Team */ public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email"; From d2410747d08a63812b49052d38967170055b395e Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Mon, 23 Jun 2025 12:11:32 -0400 Subject: [PATCH 16/47] [PM-22503] Fix manage cipher permission (#5972) * Added new tests to validate that the ciphers are being grouped and filtered correctly when assigned to multiple collections and changing order of grouping properties. --- .../Vault/Repositories/CipherRepository.cs | 5 +- .../Vault/Repositories/CipherRepository.cs | 11 +++- .../Repositories/CipherRepositoryTests.cs | 59 +++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs index e0a89b1685..9c75295f3f 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs @@ -98,7 +98,10 @@ public class CipherRepository : Repository, ICipherRepository return results .GroupBy(c => c.Id) - .Select(g => g.OrderByDescending(og => og.Edit).ThenByDescending(og => og.ViewPassword).First()) + .Select(g => + g.OrderByDescending(og => og.Manage) + .ThenByDescending(og => og.Edit) + .ThenByDescending(og => og.ViewPassword).First()) .ToList(); } } diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index befb835e26..a560e8e107 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -457,7 +457,7 @@ public class CipherRepository : Repository cipherDetailsView = withOrganizations ? + var cipherDetailsView = withOrganizations ? new UserCipherDetailsQuery(userId).Run(dbContext) : new CipherDetailsQuery(userId).Run(dbContext); if (!withOrganizations) @@ -485,8 +485,15 @@ public class CipherRepository : Repository c.Id) + .Select(g => g.OrderByDescending(c => c.Manage) + .ThenByDescending(c => c.Edit) + .ThenByDescending(c => c.ViewPassword) + .First()) + .ToList(); } } diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs index b7feeaa79b..288752011f 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs @@ -571,6 +571,65 @@ public class CipherRepositoryTests Assert.True(personalDetails.Manage, "Personal ciphers should always have Manage permission"); } + [DatabaseTheory, DatabaseData] + public async Task GetManyByUserIdAsync_WhenOneCipherIsAssignedToTwoCollectionsWithDifferentPermissions_MostPrivilegedAccessIsReturnedOnTheCipher( + ICipherRepository cipherRepository, + IUserRepository userRepository, + ICollectionCipherRepository collectionCipherRepository, + ICollectionRepository collectionRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository) + { + //Arrange + var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository); + + var cipher = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + var managedPermissionsCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "Managed", + OrganizationId = organization.Id + }); + + var unmanagedPermissionsCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "Unmanaged", + OrganizationId = organization.Id + }); + await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipher.Id, organization.Id, + [managedPermissionsCollection.Id, unmanagedPermissionsCollection.Id]); + + await collectionRepository.UpdateUsersAsync(managedPermissionsCollection.Id, new List + { + new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true } + }); + + await collectionRepository.UpdateUsersAsync(unmanagedPermissionsCollection.Id, new List + { + new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = false } + }); + + // Act + var ciphers = await cipherRepository.GetManyByUserIdAsync(user.Id); + + // Assert + Assert.Single(ciphers); + var deletableCipher = ciphers.SingleOrDefault(x => x.Id == cipher.Id); + Assert.NotNull(deletableCipher); + Assert.True(deletableCipher.Manage); + + // Annul + await cipherRepository.DeleteAsync(cipher); + await organizationUserRepository.DeleteAsync(orgUser); + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteAsync(user); + } + private async Task<(User user, Organization org, OrganizationUser orgUser)> CreateTestUserAndOrganization( IUserRepository userRepository, IOrganizationRepository organizationRepository, From 173db0a2dd8c45642bb2ecb95e89814e854300e2 Mon Sep 17 00:00:00 2001 From: Graham Walker Date: Mon, 23 Jun 2025 12:12:04 -0500 Subject: [PATCH 17/47] PM-20574 & PM-20575 Adding Risk Insight Report tables, repositories, and migrations (#5839) * PM-20574 fixing namespaces on reporting work that got moved over from tools * PM-20574 adding tables, stored procedures, and migration files * PM-20574 adding dapper and ef repos and migrations * PM-20574 changing table and repo names as requested * PM-20574 updating sql scripts to new names * PM-20574 updating sql scripts * PM-20574 updating migration script for org delete by id * PM-20574 adding mysql migration * PM-20574 updating sql migration to fix database test * PM-20574 fixing migration script * PM-20574 fixing migration script * PM-20574 fixing table scripts * PM-20574 fixing table scripts * PM-20574 fixing migration script formatting * PM-20574 fixing syntax in migration script * PM-20574 fixing file names and extensions * PM-20574 fixing sql file * PM-20574 fixing sql * PM-20574 fixing directory for entities and removing scripts from other databases * PM-20574 generating new migration scripts * PM-20574 fixed reference to a stored proc * PM-20574 adding index in scripts and missing table * PM-20574 fixing merge conflicts * PM-20574 set OUTPUT param for Id property in create and update proc * PM-20574 add CreateDate to the update proc * PM-20574 amend update proc for OrganizationApplication by adding createDate * PM-20574 formatted sql and updated as per PR comments * PM-20574 updated script to fix build error * PM-20574 fixed inconsistency in db script * PM-20574 removed revisionDate, update procedures and used views * PM-20574 removed RevisionDate from designer files * PM-20574 removed revisionDate column that was missed previously * PM-20574 added revision date back into the mix * PM-20574 updated database script to fix build error * PM-20574 fixed a procedure issue * PM-20574 fix dB build error * PM-020574 fixed additional PR comments - files cleaned up * PM-20574 updated procedure was inconsistent * Update 2025-06-13-00_OrganizationReport.sql --------- Co-authored-by: voommen-livefront --- src/Api/Dirt/Controllers/ReportsController.cs | 2 +- .../Response/MemberAccessReportModel.cs | 2 +- .../MemberCipherDetailsResponseModel.cs | 1 - .../Dirt/Entities/OrganizationApplication.cs | 20 + src/Core/Dirt/Entities/OrganizationReport.cs | 20 + .../PasswordHealthReportApplication.cs | 2 +- .../Models/Data/MemberAccessCipherDetails.cs | 2 +- .../Models/Data/MemberAccessReportDetail.cs | 0 .../Data/OrganizationMemberBaseDetail.cs | 0 .../Models/Data/RiskInsightsReportDetail.cs | 0 ...dPasswordHealthReportApplicationCommand.cs | 4 +- ...pPasswordHealthReportApplicationCommand.cs | 2 +- ...GetPasswordHealthReportApplicationQuery.cs | 4 +- ...dPasswordHealthReportApplicationCommand.cs | 2 +- ...GetPasswordHealthReportApplicationQuery.cs | 2 +- .../ReportFeatures/MemberAccessReportQuery.cs | 3 +- .../IOrganizationApplicationRepository.cs | 9 + ...IOrganizationMemberBaseDetailRepository.cs | 0 .../IOrganizationReportRepository.cs | 10 + ...sswordHealthReportApplicationRepository.cs | 4 +- .../DapperServiceCollectionExtensions.cs | 3 + .../Dirt/OrganizationApplicationRepository.cs | 35 + .../Dirt/OrganizationReportRepository.cs | 35 + ...sswordHealthReportApplicationRepository.cs | 4 +- ...ationApplicationEntityTypeConfiguration.cs | 30 + ...ganizationReportEntityTypeConfiguration.cs | 30 + .../Dirt/Models/OrganizationApplication.cs | 17 + .../Dirt/Models/OrganizationReport.cs | 17 + .../Models/PasswordHealthReportApplication.cs | 4 +- .../OrganizationApplicationRepository.cs | 29 + .../OrganizationReportRepository.cs | 30 + ...sswordHealthReportApplicationRepository.cs | 8 +- ...ityFrameworkServiceCollectionExtensions.cs | 3 + .../Repositories/DatabaseContext.cs | 3 + .../OrganizationApplication_Create.sql | 25 + .../OrganizationApplication_DeleteById.sql | 7 + .../OrganizationApplication_ReadById.sql | 9 + ...zationApplication_ReadByOrganizationId.sql | 9 + .../OrganizationApplication_Update.sql | 16 + .../OrganizationReport_Create.sql | 23 + .../OrganizationReport_DeleteById.sql | 7 + .../OrganizationReport_ReadById.sql | 9 + ...rganizationReport_ReadByOrganizationId.sql | 9 + .../Dirt/Tables/OrganizationApplication.sql | 14 + .../dbo/Dirt/Tables/OrganizationReport.sql | 18 + .../Views/OrganizationApplicationView.sql | 5 + .../dbo/Dirt/Views/OrganizationReportView.sql | 2 + .../Organization_DeleteById.sql | 14 + ...wordHealthReportApplicationCommandTests.cs | 4 +- ...wordHealthReportApplicationCommandTests.cs | 4 +- ...sswordHealthReportApplicationQueryTests.cs | 4 +- ...PasswordHealthReportApplicationFixtures.cs | 2 +- ...dHealthReportApplicationRepositoryTests.cs | 4 +- .../Infrastructure.EFIntegration.Test.csproj | 1 + .../2025-06-13-00_OrganizationReport.sql | 85 + .../2025-06-13-01_OrganizationApplication.sql | 97 + .../2025-06-13_02_UpdateOrgDeleteByIdProc.sql | 161 + ...6-13-00_OrganizationReport.sql.Designer.cs | 3252 ++++++++++++++++ ...32_2025-06-13-00_OrganizationReport.sql.cs | 95 + .../DatabaseContextModelSnapshot.cs | 80 + ...6-13-00_OrganizationReport.sql.Designer.cs | 3258 +++++++++++++++++ ...36_2025-06-13-00_OrganizationReport.sql.cs | 91 + .../DatabaseContextModelSnapshot.cs | 80 + ...6-13-00_OrganizationReport.sql.Designer.cs | 3241 ++++++++++++++++ ...39_2025-06-13-00_OrganizationReport.sql.cs | 93 + .../DatabaseContextModelSnapshot.cs | 80 + 66 files changed, 11103 insertions(+), 33 deletions(-) create mode 100644 src/Core/Dirt/Entities/OrganizationApplication.cs create mode 100644 src/Core/Dirt/Entities/OrganizationReport.cs rename src/Core/Dirt/{Reports => }/Entities/PasswordHealthReportApplication.cs (92%) rename src/Core/Dirt/{Reports => }/Models/Data/MemberAccessCipherDetails.cs (96%) rename src/Core/Dirt/{Reports => }/Models/Data/MemberAccessReportDetail.cs (100%) rename src/Core/Dirt/{Reports => }/Models/Data/OrganizationMemberBaseDetail.cs (100%) rename src/Core/Dirt/{Reports => }/Models/Data/RiskInsightsReportDetail.cs (100%) create mode 100644 src/Core/Dirt/Repositories/IOrganizationApplicationRepository.cs rename src/Core/Dirt/{Reports => }/Repositories/IOrganizationMemberBaseDetailRepository.cs (100%) create mode 100644 src/Core/Dirt/Repositories/IOrganizationReportRepository.cs rename src/Core/Dirt/{Reports => }/Repositories/IPasswordHealthReportApplicationRepository.cs (74%) create mode 100644 src/Infrastructure.Dapper/Dirt/OrganizationApplicationRepository.cs create mode 100644 src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs create mode 100644 src/Infrastructure.EntityFramework/Dirt/Configurations/OrganizationApplicationEntityTypeConfiguration.cs create mode 100644 src/Infrastructure.EntityFramework/Dirt/Configurations/OrganizationReportEntityTypeConfiguration.cs create mode 100644 src/Infrastructure.EntityFramework/Dirt/Models/OrganizationApplication.cs create mode 100644 src/Infrastructure.EntityFramework/Dirt/Models/OrganizationReport.cs create mode 100644 src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationApplicationRepository.cs create mode 100644 src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_Create.sql create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_DeleteById.sql create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_ReadById.sql create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_ReadByOrganizationId.sql create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_Update.sql create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Create.sql create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_DeleteById.sql create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_ReadById.sql create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_ReadByOrganizationId.sql create mode 100644 src/Sql/dbo/Dirt/Tables/OrganizationApplication.sql create mode 100644 src/Sql/dbo/Dirt/Tables/OrganizationReport.sql create mode 100644 src/Sql/dbo/Dirt/Views/OrganizationApplicationView.sql create mode 100644 src/Sql/dbo/Dirt/Views/OrganizationReportView.sql create mode 100644 util/Migrator/DbScripts/2025-06-13-00_OrganizationReport.sql create mode 100644 util/Migrator/DbScripts/2025-06-13-01_OrganizationApplication.sql create mode 100644 util/Migrator/DbScripts/2025-06-13_02_UpdateOrgDeleteByIdProc.sql create mode 100644 util/MySqlMigrations/Migrations/20250613215532_2025-06-13-00_OrganizationReport.sql.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250613215532_2025-06-13-00_OrganizationReport.sql.cs create mode 100644 util/PostgresMigrations/Migrations/20250613215536_2025-06-13-00_OrganizationReport.sql.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250613215536_2025-06-13-00_OrganizationReport.sql.cs create mode 100644 util/SqliteMigrations/Migrations/20250613215539_2025-06-13-00_OrganizationReport.sql.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250613215539_2025-06-13-00_OrganizationReport.sql.cs diff --git a/src/Api/Dirt/Controllers/ReportsController.cs b/src/Api/Dirt/Controllers/ReportsController.cs index 8281bdaa98..4f2697dcc5 100644 --- a/src/Api/Dirt/Controllers/ReportsController.cs +++ b/src/Api/Dirt/Controllers/ReportsController.cs @@ -2,7 +2,7 @@ using Bit.Api.Dirt.Models.Response; using Bit.Api.Tools.Models.Response; using Bit.Core.Context; -using Bit.Core.Dirt.Reports.Entities; +using Bit.Core.Dirt.Entities; using Bit.Core.Dirt.Reports.Models.Data; using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces; diff --git a/src/Api/Dirt/Models/Response/MemberAccessReportModel.cs b/src/Api/Dirt/Models/Response/MemberAccessReportModel.cs index b8356e5d44..38a5ed90d8 100644 --- a/src/Api/Dirt/Models/Response/MemberAccessReportModel.cs +++ b/src/Api/Dirt/Models/Response/MemberAccessReportModel.cs @@ -1,4 +1,4 @@ -using Bit.Core.Dirt.Reports.Models.Data; +using Bit.Core.Dirt.Models.Data; namespace Bit.Api.Dirt.Models.Response; diff --git a/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs b/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs index e5c6235de3..886cf470db 100644 --- a/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs +++ b/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs @@ -1,5 +1,4 @@ using Bit.Core.Dirt.Reports.Models.Data; - namespace Bit.Api.Dirt.Models.Response; public class MemberCipherDetailsResponseModel diff --git a/src/Core/Dirt/Entities/OrganizationApplication.cs b/src/Core/Dirt/Entities/OrganizationApplication.cs new file mode 100644 index 0000000000..259dbd60dd --- /dev/null +++ b/src/Core/Dirt/Entities/OrganizationApplication.cs @@ -0,0 +1,20 @@ +#nullable enable + +using Bit.Core.Entities; +using Bit.Core.Utilities; + +namespace Bit.Core.Dirt.Entities; + +public class OrganizationApplication : ITableObject, IRevisable +{ + public Guid Id { get; set; } + public Guid OrganizationId { get; set; } + public string Applications { get; set; } = string.Empty; + public DateTime CreationDate { get; set; } = DateTime.UtcNow; + public DateTime RevisionDate { get; set; } = DateTime.UtcNow; + + public void SetNewId() + { + Id = CoreHelpers.GenerateComb(); + } +} diff --git a/src/Core/Dirt/Entities/OrganizationReport.cs b/src/Core/Dirt/Entities/OrganizationReport.cs new file mode 100644 index 0000000000..0f327c5c8f --- /dev/null +++ b/src/Core/Dirt/Entities/OrganizationReport.cs @@ -0,0 +1,20 @@ +#nullable enable + +using Bit.Core.Entities; +using Bit.Core.Utilities; + +namespace Bit.Core.Dirt.Entities; + +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 void SetNewId() + { + Id = CoreHelpers.GenerateComb(); + } +} diff --git a/src/Core/Dirt/Reports/Entities/PasswordHealthReportApplication.cs b/src/Core/Dirt/Entities/PasswordHealthReportApplication.cs similarity index 92% rename from src/Core/Dirt/Reports/Entities/PasswordHealthReportApplication.cs rename to src/Core/Dirt/Entities/PasswordHealthReportApplication.cs index db605d6b74..5a72a0aab0 100644 --- a/src/Core/Dirt/Reports/Entities/PasswordHealthReportApplication.cs +++ b/src/Core/Dirt/Entities/PasswordHealthReportApplication.cs @@ -3,7 +3,7 @@ using Bit.Core.Entities; using Bit.Core.Utilities; -namespace Bit.Core.Dirt.Reports.Entities; +namespace Bit.Core.Dirt.Entities; public class PasswordHealthReportApplication : ITableObject, IRevisable { diff --git a/src/Core/Dirt/Reports/Models/Data/MemberAccessCipherDetails.cs b/src/Core/Dirt/Models/Data/MemberAccessCipherDetails.cs similarity index 96% rename from src/Core/Dirt/Reports/Models/Data/MemberAccessCipherDetails.cs rename to src/Core/Dirt/Models/Data/MemberAccessCipherDetails.cs index 759337d5cf..c1949ffb24 100644 --- a/src/Core/Dirt/Reports/Models/Data/MemberAccessCipherDetails.cs +++ b/src/Core/Dirt/Models/Data/MemberAccessCipherDetails.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Dirt.Reports.Models.Data; +namespace Bit.Core.Dirt.Models.Data; public class MemberAccessDetails { diff --git a/src/Core/Dirt/Reports/Models/Data/MemberAccessReportDetail.cs b/src/Core/Dirt/Models/Data/MemberAccessReportDetail.cs similarity index 100% rename from src/Core/Dirt/Reports/Models/Data/MemberAccessReportDetail.cs rename to src/Core/Dirt/Models/Data/MemberAccessReportDetail.cs diff --git a/src/Core/Dirt/Reports/Models/Data/OrganizationMemberBaseDetail.cs b/src/Core/Dirt/Models/Data/OrganizationMemberBaseDetail.cs similarity index 100% rename from src/Core/Dirt/Reports/Models/Data/OrganizationMemberBaseDetail.cs rename to src/Core/Dirt/Models/Data/OrganizationMemberBaseDetail.cs diff --git a/src/Core/Dirt/Reports/Models/Data/RiskInsightsReportDetail.cs b/src/Core/Dirt/Models/Data/RiskInsightsReportDetail.cs similarity index 100% rename from src/Core/Dirt/Reports/Models/Data/RiskInsightsReportDetail.cs rename to src/Core/Dirt/Models/Data/RiskInsightsReportDetail.cs diff --git a/src/Core/Dirt/Reports/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs index f8232ffa92..159cbb5c77 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs @@ -1,7 +1,7 @@ -using Bit.Core.Dirt.Reports.Entities; +using Bit.Core.Dirt.Entities; using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; using Bit.Core.Dirt.Reports.ReportFeatures.Requests; -using Bit.Core.Dirt.Reports.Repositories; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; using Bit.Core.Repositories; diff --git a/src/Core/Dirt/Reports/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs index 55914dca37..b955c2c958 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs @@ -1,6 +1,6 @@ using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; using Bit.Core.Dirt.Reports.ReportFeatures.Requests; -using Bit.Core.Dirt.Reports.Repositories; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; namespace Bit.Core.Dirt.Reports.ReportFeatures; diff --git a/src/Core/Dirt/Reports/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs index d9b5e79a0c..40d86338db 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs @@ -1,6 +1,6 @@ -using Bit.Core.Dirt.Reports.Entities; +using Bit.Core.Dirt.Entities; using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; -using Bit.Core.Dirt.Reports.Repositories; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; namespace Bit.Core.Dirt.Reports.ReportFeatures; diff --git a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs index 0a4aa29f2f..0fd21751de 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs @@ -1,4 +1,4 @@ -using Bit.Core.Dirt.Reports.Entities; +using Bit.Core.Dirt.Entities; using Bit.Core.Dirt.Reports.ReportFeatures.Requests; namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; diff --git a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs index ae2f759756..f7fe80b098 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs @@ -1,4 +1,4 @@ -using Bit.Core.Dirt.Reports.Entities; +using Bit.Core.Dirt.Entities; namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; diff --git a/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs index 7ab8acb8dc..21dbfc77a4 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs @@ -10,8 +10,7 @@ namespace Bit.Core.Dirt.Reports.ReportFeatures; public class MemberAccessReportQuery( IOrganizationMemberBaseDetailRepository organizationMemberBaseDetailRepository, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IApplicationCacheService applicationCacheService) - : IMemberAccessReportQuery + IApplicationCacheService applicationCacheService) : IMemberAccessReportQuery { public async Task> GetMemberAccessReportsAsync( MemberAccessReportRequest request) diff --git a/src/Core/Dirt/Repositories/IOrganizationApplicationRepository.cs b/src/Core/Dirt/Repositories/IOrganizationApplicationRepository.cs new file mode 100644 index 0000000000..f89e84e415 --- /dev/null +++ b/src/Core/Dirt/Repositories/IOrganizationApplicationRepository.cs @@ -0,0 +1,9 @@ +using Bit.Core.Dirt.Entities; +using Bit.Core.Repositories; + +namespace Bit.Core.Dirt.Repositories; + +public interface IOrganizationApplicationRepository : IRepository +{ + Task> GetByOrganizationIdAsync(Guid organizationId); +} diff --git a/src/Core/Dirt/Reports/Repositories/IOrganizationMemberBaseDetailRepository.cs b/src/Core/Dirt/Repositories/IOrganizationMemberBaseDetailRepository.cs similarity index 100% rename from src/Core/Dirt/Reports/Repositories/IOrganizationMemberBaseDetailRepository.cs rename to src/Core/Dirt/Repositories/IOrganizationMemberBaseDetailRepository.cs diff --git a/src/Core/Dirt/Repositories/IOrganizationReportRepository.cs b/src/Core/Dirt/Repositories/IOrganizationReportRepository.cs new file mode 100644 index 0000000000..6efb6cf23a --- /dev/null +++ b/src/Core/Dirt/Repositories/IOrganizationReportRepository.cs @@ -0,0 +1,10 @@ +using Bit.Core.Dirt.Entities; +using Bit.Core.Repositories; + +namespace Bit.Core.Dirt.Repositories; + +public interface IOrganizationReportRepository : IRepository +{ + Task> GetByOrganizationIdAsync(Guid organizationId); +} + diff --git a/src/Core/Dirt/Reports/Repositories/IPasswordHealthReportApplicationRepository.cs b/src/Core/Dirt/Repositories/IPasswordHealthReportApplicationRepository.cs similarity index 74% rename from src/Core/Dirt/Reports/Repositories/IPasswordHealthReportApplicationRepository.cs rename to src/Core/Dirt/Repositories/IPasswordHealthReportApplicationRepository.cs index 5b57932868..a67f696e44 100644 --- a/src/Core/Dirt/Reports/Repositories/IPasswordHealthReportApplicationRepository.cs +++ b/src/Core/Dirt/Repositories/IPasswordHealthReportApplicationRepository.cs @@ -1,7 +1,7 @@ -using Bit.Core.Dirt.Reports.Entities; +using Bit.Core.Dirt.Entities; using Bit.Core.Repositories; -namespace Bit.Core.Dirt.Reports.Repositories; +namespace Bit.Core.Dirt.Repositories; public interface IPasswordHealthReportApplicationRepository : IRepository { diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index e64eabd5bf..00728ec1a0 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using Bit.Core.Auth.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; using Bit.Core.NotificationCenter.Repositories; using Bit.Core.Platform.Installations; @@ -70,6 +71,8 @@ public static class DapperServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); if (selfHosted) diff --git a/src/Infrastructure.Dapper/Dirt/OrganizationApplicationRepository.cs b/src/Infrastructure.Dapper/Dirt/OrganizationApplicationRepository.cs new file mode 100644 index 0000000000..9f1d4d1172 --- /dev/null +++ b/src/Infrastructure.Dapper/Dirt/OrganizationApplicationRepository.cs @@ -0,0 +1,35 @@ +using System.Data; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; + +namespace Bit.Infrastructure.Dapper.Dirt; + +public class OrganizationApplicationRepository : Repository, IOrganizationApplicationRepository +{ + public OrganizationApplicationRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { + } + + public OrganizationApplicationRepository(string connectionString, string readOnlyConnectionString) + : base(connectionString, readOnlyConnectionString) + { + } + + public async Task> GetByOrganizationIdAsync(Guid organizationId) + { + using (var connection = new SqlConnection(ReadOnlyConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[OrganizationApplication_ReadByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } +} diff --git a/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs b/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs new file mode 100644 index 0000000000..88478c31ae --- /dev/null +++ b/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs @@ -0,0 +1,35 @@ +using System.Data; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; + +namespace Bit.Infrastructure.Dapper.Dirt; + +public class OrganizationReportRepository : Repository, IOrganizationReportRepository +{ + public OrganizationReportRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { + } + + public OrganizationReportRepository(string connectionString, string readOnlyConnectionString) + : base(connectionString, readOnlyConnectionString) + { + } + + public async Task> GetByOrganizationIdAsync(Guid organizationId) + { + using (var connection = new SqlConnection(ReadOnlyConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[OrganizationReport_ReadByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } +} diff --git a/src/Infrastructure.Dapper/Dirt/PasswordHealthReportApplicationRepository.cs b/src/Infrastructure.Dapper/Dirt/PasswordHealthReportApplicationRepository.cs index 2445de8a9e..266386433f 100644 --- a/src/Infrastructure.Dapper/Dirt/PasswordHealthReportApplicationRepository.cs +++ b/src/Infrastructure.Dapper/Dirt/PasswordHealthReportApplicationRepository.cs @@ -1,6 +1,6 @@ using System.Data; -using Bit.Core.Dirt.Reports.Entities; -using Bit.Core.Dirt.Reports.Repositories; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Repositories; using Bit.Core.Settings; using Bit.Infrastructure.Dapper.Repositories; using Dapper; diff --git a/src/Infrastructure.EntityFramework/Dirt/Configurations/OrganizationApplicationEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/Dirt/Configurations/OrganizationApplicationEntityTypeConfiguration.cs new file mode 100644 index 0000000000..bf0d7d011a --- /dev/null +++ b/src/Infrastructure.EntityFramework/Dirt/Configurations/OrganizationApplicationEntityTypeConfiguration.cs @@ -0,0 +1,30 @@ +using Bit.Infrastructure.EntityFramework.Dirt.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Bit.Infrastructure.EntityFramework.Dirt.Configurations; + +public class OrganizationApplicationEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .Property(s => s.Id) + .ValueGeneratedNever(); + + builder.HasIndex(s => s.Id) + .IsClustered(true); + + builder + .HasIndex(s => s.OrganizationId) + .IsClustered(false); + + builder + .HasOne(s => s.Organization) + .WithMany() + .HasForeignKey(s => s.OrganizationId) + .OnDelete(DeleteBehavior.Cascade); + + builder.ToTable(nameof(OrganizationApplication)); + } +} diff --git a/src/Infrastructure.EntityFramework/Dirt/Configurations/OrganizationReportEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/Dirt/Configurations/OrganizationReportEntityTypeConfiguration.cs new file mode 100644 index 0000000000..99794d11a2 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Dirt/Configurations/OrganizationReportEntityTypeConfiguration.cs @@ -0,0 +1,30 @@ +using Bit.Infrastructure.EntityFramework.Dirt.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Bit.Infrastructure.EntityFramework.Dirt.Configurations; + +public class OrganizationReportEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .Property(s => s.Id) + .ValueGeneratedNever(); + + builder.HasIndex(s => s.Id) + .IsClustered(true); + + builder + .HasIndex(s => s.OrganizationId) + .IsClustered(false); + + builder + .HasOne(s => s.Organization) + .WithMany() + .HasForeignKey(s => s.OrganizationId) + .OnDelete(DeleteBehavior.Cascade); + + builder.ToTable(nameof(OrganizationReport)); + } +} diff --git a/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationApplication.cs b/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationApplication.cs new file mode 100644 index 0000000000..9aaec0af2c --- /dev/null +++ b/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationApplication.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using Bit.Infrastructure.EntityFramework.AdminConsole.Models; + +namespace Bit.Infrastructure.EntityFramework.Dirt.Models; +public class OrganizationApplication : Core.Dirt.Entities.OrganizationApplication +{ + public virtual Organization Organization { get; set; } +} + +public class OrganizationApplicationProfile : Profile +{ + public OrganizationApplicationProfile() + { + CreateMap() + .ReverseMap(); + } +} diff --git a/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationReport.cs b/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationReport.cs new file mode 100644 index 0000000000..a7d08e142f --- /dev/null +++ b/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationReport.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using Bit.Infrastructure.EntityFramework.AdminConsole.Models; + +namespace Bit.Infrastructure.EntityFramework.Dirt.Models; +public class OrganizationReport : Core.Dirt.Entities.OrganizationReport +{ + public virtual Organization Organization { get; set; } +} + +public class OrganizationReportProfile : Profile +{ + public OrganizationReportProfile() + { + CreateMap() + .ReverseMap(); + } +} diff --git a/src/Infrastructure.EntityFramework/Dirt/Models/PasswordHealthReportApplication.cs b/src/Infrastructure.EntityFramework/Dirt/Models/PasswordHealthReportApplication.cs index 222bfbeb65..bc471f0844 100644 --- a/src/Infrastructure.EntityFramework/Dirt/Models/PasswordHealthReportApplication.cs +++ b/src/Infrastructure.EntityFramework/Dirt/Models/PasswordHealthReportApplication.cs @@ -3,7 +3,7 @@ using Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.Dirt.Models; -public class PasswordHealthReportApplication : Core.Dirt.Reports.Entities.PasswordHealthReportApplication +public class PasswordHealthReportApplication : Core.Dirt.Entities.PasswordHealthReportApplication { public virtual Organization Organization { get; set; } } @@ -12,7 +12,7 @@ public class PasswordHealthReportApplicationProfile : Profile { public PasswordHealthReportApplicationProfile() { - CreateMap() + CreateMap() .ReverseMap(); } } diff --git a/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationApplicationRepository.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationApplicationRepository.cs new file mode 100644 index 0000000000..f408ed85ba --- /dev/null +++ b/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationApplicationRepository.cs @@ -0,0 +1,29 @@ +using AutoMapper; +using Bit.Core.Dirt.Repositories; +using Bit.Infrastructure.EntityFramework.Dirt.Models; +using Bit.Infrastructure.EntityFramework.Repositories; +using LinqToDB; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Infrastructure.EntityFramework.Dirt.Repositories; + +public class OrganizationApplicationRepository : + Repository, + IOrganizationApplicationRepository +{ + public OrganizationApplicationRepository(IServiceScopeFactory serviceScopeFactory, + IMapper mapper) : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.OrganizationApplications) + { } + + public async Task> GetByOrganizationIdAsync(Guid organizationId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var results = await dbContext.OrganizationApplications + .Where(p => p.OrganizationId == organizationId) + .ToListAsync(); + return Mapper.Map>(results); + } + } +} diff --git a/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs new file mode 100644 index 0000000000..7c7bd56dae --- /dev/null +++ b/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs @@ -0,0 +1,30 @@ +using AutoMapper; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using LinqToDB; +using Microsoft.Extensions.DependencyInjection; + + +namespace Bit.Infrastructure.EntityFramework.Dirt.Repositories; + +public class OrganizationReportRepository : + Repository, + IOrganizationReportRepository +{ + public OrganizationReportRepository(IServiceScopeFactory serviceScopeFactory, + 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); + } + } +} diff --git a/src/Infrastructure.EntityFramework/Dirt/Repositories/PasswordHealthReportApplicationRepository.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/PasswordHealthReportApplicationRepository.cs index 604a0d87a1..296a542a8b 100644 --- a/src/Infrastructure.EntityFramework/Dirt/Repositories/PasswordHealthReportApplicationRepository.cs +++ b/src/Infrastructure.EntityFramework/Dirt/Repositories/PasswordHealthReportApplicationRepository.cs @@ -1,5 +1,5 @@ using AutoMapper; -using Bit.Core.Dirt.Reports.Repositories; +using Bit.Core.Dirt.Repositories; using Bit.Infrastructure.EntityFramework.Dirt.Models; using Bit.Infrastructure.EntityFramework.Repositories; using LinqToDB; @@ -8,14 +8,14 @@ using Microsoft.Extensions.DependencyInjection; namespace Bit.Infrastructure.EntityFramework.Dirt.Repositories; public class PasswordHealthReportApplicationRepository : - Repository, + Repository, IPasswordHealthReportApplicationRepository { public PasswordHealthReportApplicationRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.PasswordHealthReportApplications) { } - public async Task> GetByOrganizationIdAsync(Guid organizationId) + public async Task> GetByOrganizationIdAsync(Guid organizationId) { using (var scope = ServiceScopeFactory.CreateScope()) { @@ -23,7 +23,7 @@ public class PasswordHealthReportApplicationRepository : var results = await dbContext.PasswordHealthReportApplications .Where(p => p.OrganizationId == organizationId) .ToListAsync(); - return Mapper.Map>(results); + return Mapper.Map>(results); } } } diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index 616b2bc434..aab83ef7a8 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using Bit.Core.Auth.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; using Bit.Core.KeyManagement.Repositories; using Bit.Core.NotificationCenter.Repositories; @@ -108,6 +109,8 @@ public static class EntityFrameworkServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); if (selfHosted) diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs index e1e29cbf41..7446abdd97 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs @@ -16,6 +16,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using DP = Microsoft.AspNetCore.DataProtection; + #nullable enable namespace Bit.Infrastructure.EntityFramework.Repositories; @@ -84,6 +85,8 @@ public class DatabaseContext : DbContext public DbSet OrganizationMemberBaseDetails { get; set; } public DbSet SecurityTasks { get; set; } public DbSet OrganizationInstallations { get; set; } + public DbSet OrganizationReports { get; set; } + public DbSet OrganizationApplications { get; set; } protected override void OnModelCreating(ModelBuilder builder) { diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_Create.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_Create.sql new file mode 100644 index 0000000000..b2bb8593ef --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_Create.sql @@ -0,0 +1,25 @@ +CREATE PROCEDURE [dbo].[OrganizationApplication_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @Applications NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS + SET NOCOUNT ON; + + INSERT INTO [dbo].[OrganizationApplication] + ( + [Id], + [OrganizationId], + [Applications], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @OrganizationId, + @Applications, + @CreationDate, + @RevisionDate + ); diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_DeleteById.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_DeleteById.sql new file mode 100644 index 0000000000..5e00333287 --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_DeleteById.sql @@ -0,0 +1,7 @@ +CREATE PROCEDURE [dbo].[OrganizationApplication_DeleteById] + @Id UNIQUEIDENTIFIER +AS + SET NOCOUNT ON; + + DELETE FROM [dbo].[OrganizationApplication] + WHERE [Id] = @Id; diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_ReadById.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_ReadById.sql new file mode 100644 index 0000000000..13f9667df6 --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_ReadById.sql @@ -0,0 +1,9 @@ +CREATE PROCEDURE [dbo].[OrganizationApplication_ReadById] + @Id UNIQUEIDENTIFIER +AS + SET NOCOUNT ON; + + SELECT + * + FROM [dbo].[OrganizationApplicationView] + WHERE [Id] = @Id; diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_ReadByOrganizationId.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_ReadByOrganizationId.sql new file mode 100644 index 0000000000..9872c72e8d --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_ReadByOrganizationId.sql @@ -0,0 +1,9 @@ +CREATE PROCEDURE [dbo].[OrganizationApplication_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS + SET NOCOUNT ON; + + SELECT + * + FROM [dbo].[OrganizationApplicationView] + WHERE [OrganizationId] = @OrganizationId; diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_Update.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_Update.sql new file mode 100644 index 0000000000..5c24a307bd --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_Update.sql @@ -0,0 +1,16 @@ +CREATE PROCEDURE [dbo].[OrganizationApplication_Update] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @Applications NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS + SET NOCOUNT ON; + + UPDATE [dbo].[OrganizationApplication] + SET + [OrganizationId] = @OrganizationId, + [Applications] = @Applications, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate + WHERE [Id] = @Id; diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Create.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Create.sql new file mode 100644 index 0000000000..d0cea4d73b --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Create.sql @@ -0,0 +1,23 @@ +CREATE PROCEDURE [dbo].[OrganizationReport_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @Date DATETIME2(7), + @ReportData NVARCHAR(MAX), + @CreationDate DATETIME2(7) +AS + SET NOCOUNT ON; + + INSERT INTO [dbo].[OrganizationReport]( + [Id], + [OrganizationId], + [Date], + [ReportData], + [CreationDate] + ) + VALUES ( + @Id, + @OrganizationId, + @Date, + @ReportData, + @CreationDate + ); diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_DeleteById.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_DeleteById.sql new file mode 100644 index 0000000000..fa274424df --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_DeleteById.sql @@ -0,0 +1,7 @@ +CREATE PROCEDURE [dbo].[OrganizationReport_DeleteById] + @Id UNIQUEIDENTIFIER +AS + SET NOCOUNT ON; + + DELETE FROM [dbo].[OrganizationReport] + WHERE [Id] = @Id diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_ReadById.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_ReadById.sql new file mode 100644 index 0000000000..046172a4c3 --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_ReadById.sql @@ -0,0 +1,9 @@ +CREATE PROCEDURE [dbo].[OrganizationReport_ReadById] + @Id UNIQUEIDENTIFIER +AS + SET NOCOUNT ON; + + SELECT + * + FROM [dbo].[OrganizationReportView] + WHERE [Id] = @Id; diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_ReadByOrganizationId.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_ReadByOrganizationId.sql new file mode 100644 index 0000000000..6bdcf51f70 --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_ReadByOrganizationId.sql @@ -0,0 +1,9 @@ +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/Tables/OrganizationApplication.sql b/src/Sql/dbo/Dirt/Tables/OrganizationApplication.sql new file mode 100644 index 0000000000..58c8080e23 --- /dev/null +++ b/src/Sql/dbo/Dirt/Tables/OrganizationApplication.sql @@ -0,0 +1,14 @@ +CREATE TABLE [dbo].[OrganizationApplication] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [Applications] NVARCHAR(MAX) NOT NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [RevisionDate] DATETIME2 (7) 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] + ON [dbo].[OrganizationApplication]([OrganizationId] ASC); +GO diff --git a/src/Sql/dbo/Dirt/Tables/OrganizationReport.sql b/src/Sql/dbo/Dirt/Tables/OrganizationReport.sql new file mode 100644 index 0000000000..563877a340 --- /dev/null +++ b/src/Sql/dbo/Dirt/Tables/OrganizationReport.sql @@ -0,0 +1,18 @@ +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, + 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); +GO + +CREATE NONCLUSTERED INDEX [IX_OrganizationReport_OrganizationId_Date] + ON [dbo].[OrganizationReport]([OrganizationId] ASC, [Date] DESC); +GO diff --git a/src/Sql/dbo/Dirt/Views/OrganizationApplicationView.sql b/src/Sql/dbo/Dirt/Views/OrganizationApplicationView.sql new file mode 100644 index 0000000000..af687ceb8e --- /dev/null +++ b/src/Sql/dbo/Dirt/Views/OrganizationApplicationView.sql @@ -0,0 +1,5 @@ +CREATE VIEW [dbo].[OrganizationApplicationView] +AS +SELECT + * +FROM [dbo].[OrganizationApplication]; diff --git a/src/Sql/dbo/Dirt/Views/OrganizationReportView.sql b/src/Sql/dbo/Dirt/Views/OrganizationReportView.sql new file mode 100644 index 0000000000..202dccd35e --- /dev/null +++ b/src/Sql/dbo/Dirt/Views/OrganizationReportView.sql @@ -0,0 +1,2 @@ +CREATE VIEW [dbo].[OrganizationReportView] AS +SELECT * FROM [dbo].[OrganizationReport]; diff --git a/src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql b/src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql index 2daa12209f..50ad247c1a 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql @@ -137,6 +137,20 @@ BEGIN WHERE [OrganizationId] = @Id + -- Delete Organization Application + DELETE + FROM + [dbo].[OrganizationApplication] + WHERE + [OrganizationId] = @Id + + -- Delete Organization Report + DELETE + FROM + [dbo].[OrganizationReport] + WHERE + [OrganizationId] = @Id + DELETE FROM [dbo].[Organization] diff --git a/test/Core.Test/Dirt/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs b/test/Core.Test/Dirt/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs index be54bc5310..849732685c 100644 --- a/test/Core.Test/Dirt/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs +++ b/test/Core.Test/Dirt/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs @@ -1,9 +1,9 @@ using AutoFixture; using Bit.Core.AdminConsole.Entities; -using Bit.Core.Dirt.Reports.Entities; +using Bit.Core.Dirt.Entities; using Bit.Core.Dirt.Reports.ReportFeatures; using Bit.Core.Dirt.Reports.ReportFeatures.Requests; -using Bit.Core.Dirt.Reports.Repositories; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Test.Common.AutoFixture; diff --git a/test/Core.Test/Dirt/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs b/test/Core.Test/Dirt/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs index 7756995805..6265f463c7 100644 --- a/test/Core.Test/Dirt/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs +++ b/test/Core.Test/Dirt/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs @@ -1,8 +1,8 @@ using AutoFixture; -using Bit.Core.Dirt.Reports.Entities; +using Bit.Core.Dirt.Entities; using Bit.Core.Dirt.Reports.ReportFeatures; using Bit.Core.Dirt.Reports.ReportFeatures.Requests; -using Bit.Core.Dirt.Reports.Repositories; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Core.Test/Dirt/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs b/test/Core.Test/Dirt/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs index eddfd6c1bc..8513ddb9cb 100644 --- a/test/Core.Test/Dirt/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs +++ b/test/Core.Test/Dirt/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs @@ -1,7 +1,7 @@ using AutoFixture; -using Bit.Core.Dirt.Reports.Entities; +using Bit.Core.Dirt.Entities; using Bit.Core.Dirt.Reports.ReportFeatures; -using Bit.Core.Dirt.Reports.Repositories; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Infrastructure.EFIntegration.Test/AutoFixture/PasswordHealthReportApplicationFixtures.cs b/test/Infrastructure.EFIntegration.Test/AutoFixture/PasswordHealthReportApplicationFixtures.cs index f6100fc71f..31ab99dc4a 100644 --- a/test/Infrastructure.EFIntegration.Test/AutoFixture/PasswordHealthReportApplicationFixtures.cs +++ b/test/Infrastructure.EFIntegration.Test/AutoFixture/PasswordHealthReportApplicationFixtures.cs @@ -1,6 +1,6 @@ using AutoFixture; using AutoFixture.Kernel; -using Bit.Core.Dirt.Reports.Entities; +using Bit.Core.Dirt.Entities; using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; using Bit.Infrastructure.EntityFramework.Dirt.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; diff --git a/test/Infrastructure.EFIntegration.Test/Dirt/Repositories/PasswordHealthReportApplicationRepositoryTests.cs b/test/Infrastructure.EFIntegration.Test/Dirt/Repositories/PasswordHealthReportApplicationRepositoryTests.cs index d796635153..5dfbb3e942 100644 --- a/test/Infrastructure.EFIntegration.Test/Dirt/Repositories/PasswordHealthReportApplicationRepositoryTests.cs +++ b/test/Infrastructure.EFIntegration.Test/Dirt/Repositories/PasswordHealthReportApplicationRepositoryTests.cs @@ -1,7 +1,7 @@ using AutoFixture; using Bit.Core.AdminConsole.Entities; -using Bit.Core.Dirt.Reports.Entities; -using Bit.Core.Dirt.Reports.Repositories; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Repositories; using Bit.Core.Repositories; using Bit.Core.Test.AutoFixture.Attributes; using Bit.Infrastructure.Dapper.Dirt; diff --git a/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj b/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj index e63d3d7419..8c2eb50c5b 100644 --- a/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj +++ b/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj @@ -30,6 +30,7 @@ + diff --git a/util/Migrator/DbScripts/2025-06-13-00_OrganizationReport.sql b/util/Migrator/DbScripts/2025-06-13-00_OrganizationReport.sql new file mode 100644 index 0000000000..ad256c3ea6 --- /dev/null +++ b/util/Migrator/DbScripts/2025-06-13-00_OrganizationReport.sql @@ -0,0 +1,85 @@ +IF OBJECT_ID('dbo.OrganizationReport') IS NULL +BEGIN + 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, + CONSTRAINT [PK_OrganizationReport] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_OrganizationReport_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) + ); + + CREATE NONCLUSTERED INDEX [IX_OrganizationReport_OrganizationId] ON [dbo].[OrganizationReport]([OrganizationId] ASC); + + CREATE NONCLUSTERED INDEX [IX_OrganizationReport_OrganizationId_Date] ON [dbo].[OrganizationReport]([OrganizationId] ASC, [Date] DESC); + +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) +AS + SET NOCOUNT ON; + + INSERT INTO [dbo].[OrganizationReport]( + [Id], + [OrganizationId], + [Date], + [ReportData], + [CreationDate] + ) + VALUES ( + @Id, + @OrganizationId, + @Date, + @ReportData, + @CreationDate + ); +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationReport_DeleteById] + @Id UNIQUEIDENTIFIER +AS + SET NOCOUNT ON; + + DELETE FROM [dbo].[OrganizationReport] + WHERE [Id] = @Id; + +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationReport_ReadById] + @Id UNIQUEIDENTIFIER +AS + SET NOCOUNT ON; + + SELECT + * + FROM [dbo].[OrganizationReportView] + WHERE [Id] = @Id; + +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationReport_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS + SET NOCOUNT ON; + + SELECT + * + FROM [dbo].[OrganizationReportView] + WHERE [OrganizationId] = @OrganizationId; +GO diff --git a/util/Migrator/DbScripts/2025-06-13-01_OrganizationApplication.sql b/util/Migrator/DbScripts/2025-06-13-01_OrganizationApplication.sql new file mode 100644 index 0000000000..2f2a357ad3 --- /dev/null +++ b/util/Migrator/DbScripts/2025-06-13-01_OrganizationApplication.sql @@ -0,0 +1,97 @@ +IF OBJECT_ID('dbo.OrganizationApplication') IS NULL +BEGIN + CREATE TABLE [dbo].[OrganizationApplication] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [Applications] NVARCHAR(MAX) NOT NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [RevisionDate] DATETIME2 (7) NOT NULL, + CONSTRAINT [PK_OrganizationApplication] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_OrganizationApplication_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) + ); + + CREATE NONCLUSTERED INDEX [IX_OrganizationApplication_OrganizationId] + ON [dbo].[OrganizationApplication]([OrganizationId] ASC); +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) +AS + SET NOCOUNT ON; + + INSERT INTO [dbo].[OrganizationApplication] + ( + [Id], + [OrganizationId], + [Applications], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @OrganizationId, + @Applications, + @CreationDate, + @RevisionDate + ); +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationApplication_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS + SET NOCOUNT ON; + + SELECT + * + FROM [dbo].[OrganizationApplicationView] + WHERE [OrganizationId] = @OrganizationId; +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationApplication_ReadById] + @Id UNIQUEIDENTIFIER +AS + SET NOCOUNT ON; + + SELECT + * + FROM [dbo].[OrganizationApplicationView] + WHERE [Id] = @Id; +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationApplication_Update] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @Applications NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS + SET NOCOUNT ON; + + UPDATE [dbo].[OrganizationApplication] + SET + [OrganizationId] = @OrganizationId, + [Applications] = @Applications, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate + WHERE [Id] = @Id; + +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationApplication_DeleteById] + @Id UNIQUEIDENTIFIER +AS + SET NOCOUNT ON; + + DELETE FROM [dbo].[OrganizationApplication] + WHERE [Id] = @Id; +GO diff --git a/util/Migrator/DbScripts/2025-06-13_02_UpdateOrgDeleteByIdProc.sql b/util/Migrator/DbScripts/2025-06-13_02_UpdateOrgDeleteByIdProc.sql new file mode 100644 index 0000000000..70e6e7a2c6 --- /dev/null +++ b/util/Migrator/DbScripts/2025-06-13_02_UpdateOrgDeleteByIdProc.sql @@ -0,0 +1,161 @@ +CREATE OR ALTER PROCEDURE [dbo].[Organization_DeleteById] + @Id UNIQUEIDENTIFIER +WITH RECOMPILE +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @Id + + DECLARE @BatchSize INT = 100 + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION Organization_DeleteById_Ciphers + + DELETE TOP(@BatchSize) + FROM + [dbo].[Cipher] + WHERE + [UserId] IS NULL + AND [OrganizationId] = @Id + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION Organization_DeleteById_Ciphers + END + + BEGIN TRANSACTION Organization_DeleteById + + DELETE + FROM + [dbo].[AuthRequest] + WHERE + [OrganizationId] = @Id + + DELETE + FROM + [dbo].[SsoUser] + WHERE + [OrganizationId] = @Id + + DELETE + FROM + [dbo].[SsoConfig] + WHERE + [OrganizationId] = @Id + + DELETE CU + FROM + [dbo].[CollectionUser] CU + INNER JOIN + [dbo].[OrganizationUser] OU ON [CU].[OrganizationUserId] = [OU].[Id] + WHERE + [OU].[OrganizationId] = @Id + + DELETE AP + FROM + [dbo].[AccessPolicy] AP + INNER JOIN + [dbo].[OrganizationUser] OU ON [AP].[OrganizationUserId] = [OU].[Id] + WHERE + [OU].[OrganizationId] = @Id + + DELETE GU + FROM + [dbo].[GroupUser] GU + INNER JOIN + [dbo].[OrganizationUser] OU ON [GU].[OrganizationUserId] = [OU].[Id] + WHERE + [OU].[OrganizationId] = @Id + + DELETE + FROM + [dbo].[OrganizationUser] + WHERE + [OrganizationId] = @Id + + DELETE + FROM + [dbo].[ProviderOrganization] + WHERE + [OrganizationId] = @Id + + EXEC [dbo].[OrganizationApiKey_OrganizationDeleted] @Id + EXEC [dbo].[OrganizationConnection_OrganizationDeleted] @Id + EXEC [dbo].[OrganizationSponsorship_OrganizationDeleted] @Id + EXEC [dbo].[OrganizationDomain_OrganizationDeleted] @Id + EXEC [dbo].[OrganizationIntegration_OrganizationDeleted] @Id + + DELETE + FROM + [dbo].[Project] + WHERE + [OrganizationId] = @Id + + DELETE + FROM + [dbo].[Secret] + WHERE + [OrganizationId] = @Id + + DELETE AK + FROM + [dbo].[ApiKey] AK + INNER JOIN + [dbo].[ServiceAccount] SA ON [AK].[ServiceAccountId] = [SA].[Id] + WHERE + [SA].[OrganizationId] = @Id + + DELETE AP + FROM + [dbo].[AccessPolicy] AP + INNER JOIN + [dbo].[ServiceAccount] SA ON [AP].[GrantedServiceAccountId] = [SA].[Id] + WHERE + [SA].[OrganizationId] = @Id + + DELETE + FROM + [dbo].[ServiceAccount] + WHERE + [OrganizationId] = @Id + + -- Delete Notification Status + DELETE + NS + FROM + [dbo].[NotificationStatus] NS + INNER JOIN + [dbo].[Notification] N ON N.[Id] = NS.[NotificationId] + WHERE + N.[OrganizationId] = @Id + + -- Delete Notification + DELETE + FROM + [dbo].[Notification] + WHERE + [OrganizationId] = @Id + + -- Delete Organization Application + DELETE + FROM + [dbo].[OrganizationApplication] + WHERE + [OrganizationId] = @Id + + -- Delete Organization Report + DELETE + FROM + [dbo].[OrganizationReport] + WHERE + [OrganizationId] = @Id + + DELETE + FROM + [dbo].[Organization] + WHERE + [Id] = @Id + COMMIT TRANSACTION Organization_DeleteById + END + GO diff --git a/util/MySqlMigrations/Migrations/20250613215532_2025-06-13-00_OrganizationReport.sql.Designer.cs b/util/MySqlMigrations/Migrations/20250613215532_2025-06-13-00_OrganizationReport.sql.Designer.cs new file mode 100644 index 0000000000..0c8987658f --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250613215532_2025-06-13-00_OrganizationReport.sql.Designer.cs @@ -0,0 +1,3252 @@ +// +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("20250613215532_2025-06-13-00_OrganizationReport.sql")] + partial class _2025061300_OrganizationReportsql + { + /// + 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("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("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("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/20250613215532_2025-06-13-00_OrganizationReport.sql.cs b/util/MySqlMigrations/Migrations/20250613215532_2025-06-13-00_OrganizationReport.sql.cs new file mode 100644 index 0000000000..13650faf47 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250613215532_2025-06-13-00_OrganizationReport.sql.cs @@ -0,0 +1,95 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class _2025061300_OrganizationReportsql : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OrganizationApplication", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + OrganizationId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Applications = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreationDate = table.Column(type: "datetime(6)", nullable: false), + RevisionDate = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationApplication", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationApplication_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "OrganizationReport", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + OrganizationId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Date = table.Column(type: "datetime(6)", nullable: false), + ReportData = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreationDate = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationReport", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationReport_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationApplication_Id", + table: "OrganizationApplication", + column: "Id"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationApplication_OrganizationId", + table: "OrganizationApplication", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationReport_Id", + table: "OrganizationReport", + column: "Id"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationReport_OrganizationId", + table: "OrganizationReport", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationReport_OrganizationId_Date", + table: "OrganizationReport", + columns: ["OrganizationId", "Date"], + descending: [false, true]); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrganizationApplication"); + + migrationBuilder.DropTable( + name: "OrganizationReport"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index c4b41921b1..ed8f56f7e6 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -967,6 +967,64 @@ namespace Bit.MySqlMigrations.Migrations 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("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("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") @@ -2579,6 +2637,28 @@ namespace Bit.MySqlMigrations.Migrations 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") diff --git a/util/PostgresMigrations/Migrations/20250613215536_2025-06-13-00_OrganizationReport.sql.Designer.cs b/util/PostgresMigrations/Migrations/20250613215536_2025-06-13-00_OrganizationReport.sql.Designer.cs new file mode 100644 index 0000000000..c8d49c17f8 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250613215536_2025-06-13-00_OrganizationReport.sql.Designer.cs @@ -0,0 +1,3258 @@ +// +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("20250613215536_2025-06-13-00_OrganizationReport.sql")] + partial class _2025061300_OrganizationReportsql + { + /// + 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("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("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("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/20250613215536_2025-06-13-00_OrganizationReport.sql.cs b/util/PostgresMigrations/Migrations/20250613215536_2025-06-13-00_OrganizationReport.sql.cs new file mode 100644 index 0000000000..55fe031009 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250613215536_2025-06-13-00_OrganizationReport.sql.cs @@ -0,0 +1,91 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class _2025061300_OrganizationReportsql : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OrganizationApplication", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrganizationId = table.Column(type: "uuid", nullable: false), + Applications = table.Column(type: "text", nullable: false), + CreationDate = table.Column(type: "timestamp with time zone", nullable: false), + RevisionDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationApplication", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationApplication_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "OrganizationReport", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrganizationId = table.Column(type: "uuid", nullable: false), + Date = table.Column(type: "timestamp with time zone", nullable: false), + ReportData = table.Column(type: "text", nullable: false), + CreationDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationReport", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationReport_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationApplication_Id", + table: "OrganizationApplication", + column: "Id"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationApplication_OrganizationId", + table: "OrganizationApplication", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationReport_Id", + table: "OrganizationReport", + column: "Id"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationReport_OrganizationId", + table: "OrganizationReport", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationReport_OrganizationId_Date", + table: "OrganizationReport", + columns: ["OrganizationId", "Date"], + descending: [false, true]); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrganizationApplication"); + + migrationBuilder.DropTable( + name: "OrganizationReport"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index c4dcce5070..aa283cf352 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -972,6 +972,64 @@ namespace Bit.PostgresMigrations.Migrations 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("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("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") @@ -2585,6 +2643,28 @@ namespace Bit.PostgresMigrations.Migrations 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") diff --git a/util/SqliteMigrations/Migrations/20250613215539_2025-06-13-00_OrganizationReport.sql.Designer.cs b/util/SqliteMigrations/Migrations/20250613215539_2025-06-13-00_OrganizationReport.sql.Designer.cs new file mode 100644 index 0000000000..f6c241ff5e --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250613215539_2025-06-13-00_OrganizationReport.sql.Designer.cs @@ -0,0 +1,3241 @@ +// +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("20250613215539_2025-06-13-00_OrganizationReport.sql")] + partial class _2025061300_OrganizationReportsql + { + /// + 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("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("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("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/20250613215539_2025-06-13-00_OrganizationReport.sql.cs b/util/SqliteMigrations/Migrations/20250613215539_2025-06-13-00_OrganizationReport.sql.cs new file mode 100644 index 0000000000..c2e7afdc22 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250613215539_2025-06-13-00_OrganizationReport.sql.cs @@ -0,0 +1,93 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class _2025061300_OrganizationReportsql : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OrganizationApplication", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + Applications = table.Column(type: "TEXT", nullable: false), + CreationDate = table.Column(type: "TEXT", nullable: false), + RevisionDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationApplication", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationApplication_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "OrganizationReport", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + Date = table.Column(type: "TEXT", nullable: false), + ReportData = table.Column(type: "TEXT", nullable: false), + CreationDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationReport", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationReport_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationApplication_Id", + table: "OrganizationApplication", + column: "Id"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationApplication_OrganizationId", + table: "OrganizationApplication", + column: "OrganizationId"); + + + migrationBuilder.CreateIndex( + name: "IX_OrganizationReport_Id", + table: "OrganizationReport", + column: "Id"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationReport_OrganizationId", + table: "OrganizationReport", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationReport_OrganizationId_Date", + table: "OrganizationReport", + columns: ["OrganizationId", "Date"], + descending: [false, true]); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrganizationApplication"); + + migrationBuilder.DropTable( + name: "OrganizationReport"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 611e042a1f..ef9fb7cbfd 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -956,6 +956,64 @@ namespace Bit.SqliteMigrations.Migrations 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("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("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") @@ -2568,6 +2626,28 @@ namespace Bit.SqliteMigrations.Migrations 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") From 2da1b43c10158753087be36c14697622b6f2e885 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:23:23 -0400 Subject: [PATCH 18/47] Devcontainer restart enhancements (#5965) * Add environment variable options to quickly start the devcontainer Co-authored-by: tangowithfoxtrot * Fix DEV_CERT_CONTENTS --------- Co-authored-by: tangowithfoxtrot --- .../bitwarden_common/docker-compose.yml | 3 + .../internal_dev/postCreateCommand.sh | 125 ++++++++++++------ dev/.env.example | 9 ++ 3 files changed, 93 insertions(+), 44 deletions(-) diff --git a/.devcontainer/bitwarden_common/docker-compose.yml b/.devcontainer/bitwarden_common/docker-compose.yml index 2f3a62877e..fbfca10d21 100644 --- a/.devcontainer/bitwarden_common/docker-compose.yml +++ b/.devcontainer/bitwarden_common/docker-compose.yml @@ -3,6 +3,9 @@ services: image: mcr.microsoft.com/devcontainers/dotnet:8.0 volumes: - ../../:/workspace:cached + env_file: + - path: ../../dev/.env + required: false # Overrides default command so things don't shut down after the process ends. command: sleep infinity diff --git a/.devcontainer/internal_dev/postCreateCommand.sh b/.devcontainer/internal_dev/postCreateCommand.sh index 071ffc0b29..3fd278be26 100755 --- a/.devcontainer/internal_dev/postCreateCommand.sh +++ b/.devcontainer/internal_dev/postCreateCommand.sh @@ -1,17 +1,42 @@ #!/usr/bin/env bash -export DEV_DIR=/workspace/dev +export REPO_ROOT="$(git rev-parse --show-toplevel)" export CONTAINER_CONFIG=/workspace/.devcontainer/internal_dev + git config --global --add safe.directory /workspace -get_installation_id_and_key() { - pushd ./dev >/dev/null || exit - echo "Please enter your installation id and key from https://bitwarden.com/host:" - read -r -p "Installation id: " INSTALLATION_ID - read -r -p "Installation key: " INSTALLATION_KEY - jq ".globalSettings.installation.id = \"$INSTALLATION_ID\" | - .globalSettings.installation.key = \"$INSTALLATION_KEY\"" \ - secrets.json.example >secrets.json # create/overwrite secrets.json - popd >/dev/null || exit +if [[ -z "${CODESPACES}" ]]; then + allow_interactive=1 +else + echo "Doing non-interactive setup" + allow_interactive=0 +fi + +get_option() { + # Helper function for reading the value of an environment variable + # primarily but then falling back to an interactive question if allowed + # and lastly falling back to a default value input when either other + # option is available. + name_of_var="$1" + question_text="$2" + default_value="$3" + is_secret="$4" + + if [[ -n "${!name_of_var}" ]]; then + # If the env variable they gave us has a value, then use that value + echo "${!name_of_var}" + elif [[ "$allow_interactive" == 1 ]]; then + # If we can be interactive, then use the text they gave us to request input + if [[ "$is_secret" == 1 ]]; then + read -r -s -p "$question_text" response + echo "$response" + else + read -r -p "$question_text" response + echo "$response" + fi + else + # If no environment variable and not interactive, then just give back default value + echo "$default_value" + fi } remove_comments() { @@ -26,51 +51,70 @@ remove_comments() { configure_other_vars() { pushd ./dev >/dev/null || exit - cp secrets.json .secrets.json.tmp + cp "$REPO_ROOT/dev/secrets.json" "$REPO_ROOT/dev/.secrets.json.tmp" # set DB_PASSWORD equal to .services.mssql.environment.MSSQL_SA_PASSWORD, accounting for quotes - DB_PASSWORD="$(grep -oP 'MSSQL_SA_PASSWORD=["'"'"']?\K[^"'"'"'\s]+' $DEV_DIR/.env)" + DB_PASSWORD="$(grep -oP 'MSSQL_SA_PASSWORD=["'"'"']?\K[^"'"'"'\s]+' $REPO_ROOT/dev/.env)" SQL_CONNECTION_STRING="Server=localhost;Database=vault_dev;User Id=SA;Password=$DB_PASSWORD;Encrypt=True;TrustServerCertificate=True" jq \ ".globalSettings.sqlServer.connectionString = \"$SQL_CONNECTION_STRING\" | .globalSettings.postgreSql.connectionString = \"Host=localhost;Username=postgres;Password=$DB_PASSWORD;Database=vault_dev;Include Error Detail=true\" | .globalSettings.mySql.connectionString = \"server=localhost;uid=root;pwd=$DB_PASSWORD;database=vault_dev\"" \ .secrets.json.tmp >secrets.json - rm .secrets.json.tmp + rm "$REPO_ROOT/dev/.secrets.json.tmp" popd >/dev/null || exit } one_time_setup() { - read -r -p \ - "Would you like to configure your secrets and certificates for the first time? -WARNING: This will overwrite any existing secrets.json and certificate files. -Proceed? [y/N] " response - if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then - echo "Running one-time setup script..." - sleep 1 - read -r -p \ - "Place the secrets.json and dev.pfx files from our shared Collection in the ./dev directory. + if [[ ! -f "$REPO_ROOT/dev/dev.pfx" ]]; then + # We do not have the cert file + if [[ ! -z "${DEV_CERT_CONTENTS}" ]]; then + # Make file for them + echo "Making $REPO_ROOT/dev/dev.pfx file for you based on DEV_CERT_CONTENTS environment variable." + # Assume content is base64 encoded + echo "$DEV_CERT_CONTENTS" | base64 -d > "$REPO_ROOT/dev/dev.pfx" + else + if [[ $allow_interactive -eq 1 ]]; then + read -r -p \ + "Place the dev.pfx files from our shared Collection in the $REPO_ROOT/dev directory. Press to continue." - remove_comments ./dev/secrets.json + fi + fi + fi + + if [[ -f "$REPO_ROOT/dev/dev.pfx" ]]; then + dotnet tool install dotnet-certificate-tool -g >/dev/null + cert_password="$(get_option "DEV_CERT_PASSWORD" "Paste the \"Licensing Certificate - Dev\" password: " "" 1)" + certificate-tool add --file "$REPO_ROOT/dev/dev.pfx" --password "$cert_password" + else + echo "You don't have a $REPO_ROOT/dev/dev.pfx file setup." >/dev/stderr + fi + + do_secrets_json_setup="$(get_option "SETUP_SECRETS_JSON" "Would you like us to setup your secrets.json file for you? [y/N] " "n")" + if [[ "$do_secrets_json_setup" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + remove_comments "$REPO_ROOT/dev/secrets.json" configure_other_vars + # setup_secrets needs to be ran from the dev folder + pushd "$REPO_ROOT/dev" >/dev/null || exit + echo "Injecting dotnet secrets..." + pwsh "$REPO_ROOT/dev/setup_secrets.ps1" || true + popd >/dev/null || exit + fi + + do_azurite_setup="$(get_option "SETUP_AZURITE" "Would you like us to setup your azurite environment? [y/N] " "n")" + if [[ "$do_azurite_setup" =~ ^([yY][eE][sS]|[yY])+$ ]]; then echo "Installing Az module. This will take ~a minute..." pwsh -Command "Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -Force" - pwsh ./dev/setup_azurite.ps1 - - dotnet tool install dotnet-certificate-tool -g >/dev/null - - read -r -s -p "Paste the \"Licensing Certificate - Dev\" password: " CERT_PASSWORD - echo - pushd ./dev >/dev/null || exit - certificate-tool add --file ./dev.pfx --password "$CERT_PASSWORD" - echo "Injecting dotnet secrets..." - pwsh ./setup_secrets.ps1 || true - popd >/dev/null || exit + pwsh "$REPO_ROOT/dev/setup_azurite.ps1" + fi + run_mssql_migrations="$(get_option "RUN_MSSQL_MIGRATIONS" "Would you like us to run MSSQL Migrations for you? [y/N] " "n")" + if [[ "$do_azurite_setup" =~ ^([yY][eE][sS]|[yY])+$ ]]; then echo "Running migrations..." sleep 5 # wait for DB container to start - dotnet run --project ./util/MsSqlMigratorUtility "$SQL_CONNECTION_STRING" + dotnet run --project "$REPO_ROOT/util/MsSqlMigratorUtility" "$SQL_CONNECTION_STRING" fi - read -r -p "Would you like to install the Stripe CLI? [y/N] " stripe_response + + stripe_response="$(get_option "INSTALL_STRIPE_CLI" "Would you like to install the Stripe CLI? [y/N] " "n")" if [[ "$stripe_response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then install_stripe_cli fi @@ -88,11 +132,4 @@ install_stripe_cli() { sudo apt install -y stripe } -# main -if [[ -z "${CODESPACES}" ]]; then - one_time_setup -else - # Ignore interactive elements when running in codespaces since they are not supported there - # TODO Write codespaces specific instructions and link here - echo "Running in codespaces, follow instructions here: https://contributing.bitwarden.com/getting-started/server/guide/ to continue the setup" -fi \ No newline at end of file +one_time_setup diff --git a/dev/.env.example b/dev/.env.example index 7f049728d7..f31b5b9eeb 100644 --- a/dev/.env.example +++ b/dev/.env.example @@ -26,3 +26,12 @@ IDENTITY_PROXY_PORT=33756 # Optional RabbitMQ configuration RABBITMQ_DEFAULT_USER=bitwarden RABBITMQ_DEFAULT_PASS=SET_A_PASSWORD_HERE_123 + +# Environment variables that help customize dev container start +# Without these the dev container will ask these questions in an interactive manner +# when possible (excluding running in GitHub Codespaces) +# SETUP_SECRETS_JSON=yes +# SETUP_AZURITE=yes +# RUN_MSSQL_MIGRATIONS=yes +# DEV_CERT_PASSWORD=dev_cert_password_here +# INSTALL_STRIPE_CLI=no From 77bf849d85cd312c8b46cae23bb4516e479ebb2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:19:48 +0100 Subject: [PATCH 19/47] [PM-22105] Extract CollectionService.SaveAsync into commands (#5959) * 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 --- src/Api/Controllers/CollectionsController.cs | 10 +- .../Controllers/CollectionsController.cs | 9 +- .../CreateCollectionCommand.cs | 70 ++++++ .../Interfaces/ICreateCollectionCommand.cs | 17 ++ .../Interfaces/IUpdateCollectionCommand.cs | 17 ++ .../UpdateCollectionCommand.cs | 59 +++++ ...OrganizationServiceCollectionExtensions.cs | 2 + src/Core/Services/ICollectionService.cs | 2 - .../Implementations/CollectionService.cs | 52 ----- .../Controllers/CollectionsControllerTests.cs | 11 +- .../CreateCollectionCommandTests.cs | 202 ++++++++++++++++++ .../UpdateCollectionCommandTests.cs | 169 +++++++++++++++ .../Services/CollectionServiceTests.cs | 126 ----------- 13 files changed, 555 insertions(+), 191 deletions(-) create mode 100644 src/Core/OrganizationFeatures/OrganizationCollections/CreateCollectionCommand.cs create mode 100644 src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/ICreateCollectionCommand.cs create mode 100644 src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/IUpdateCollectionCommand.cs create mode 100644 src/Core/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommand.cs create mode 100644 test/Core.Test/OrganizationFeatures/OrganizationCollections/CreateCollectionCommandTests.cs create mode 100644 test/Core.Test/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommandTests.cs diff --git a/src/Api/Controllers/CollectionsController.cs b/src/Api/Controllers/CollectionsController.cs index e0f1c0d2c8..e18999c1e0 100644 --- a/src/Api/Controllers/CollectionsController.cs +++ b/src/Api/Controllers/CollectionsController.cs @@ -20,6 +20,8 @@ public class CollectionsController : Controller { private readonly ICollectionRepository _collectionRepository; private readonly ICollectionService _collectionService; + private readonly ICreateCollectionCommand _createCollectionCommand; + private readonly IUpdateCollectionCommand _updateCollectionCommand; private readonly IDeleteCollectionCommand _deleteCollectionCommand; private readonly IUserService _userService; private readonly IAuthorizationService _authorizationService; @@ -29,6 +31,8 @@ public class CollectionsController : Controller public CollectionsController( ICollectionRepository collectionRepository, ICollectionService collectionService, + ICreateCollectionCommand createCollectionCommand, + IUpdateCollectionCommand updateCollectionCommand, IDeleteCollectionCommand deleteCollectionCommand, IUserService userService, IAuthorizationService authorizationService, @@ -37,6 +41,8 @@ public class CollectionsController : Controller { _collectionRepository = collectionRepository; _collectionService = collectionService; + _createCollectionCommand = createCollectionCommand; + _updateCollectionCommand = updateCollectionCommand; _deleteCollectionCommand = deleteCollectionCommand; _userService = userService; _authorizationService = authorizationService; @@ -153,7 +159,7 @@ public class CollectionsController : Controller var groups = model.Groups?.Select(g => g.ToSelectionReadOnly()); var users = model.Users?.Select(g => g.ToSelectionReadOnly()).ToList() ?? new List(); - await _collectionService.SaveAsync(collection, groups, users); + await _createCollectionCommand.CreateAsync(collection, groups, users); if (!_currentContext.UserId.HasValue || (_currentContext.GetOrganization(orgId) == null && await _currentContext.ProviderUserForOrgAsync(orgId))) { @@ -179,7 +185,7 @@ public class CollectionsController : Controller var groups = model.Groups?.Select(g => g.ToSelectionReadOnly()); var users = model.Users?.Select(g => g.ToSelectionReadOnly()); - await _collectionService.SaveAsync(model.ToCollection(collection), groups, users); + await _updateCollectionCommand.UpdateAsync(model.ToCollection(collection), groups, users); if (!_currentContext.UserId.HasValue || (_currentContext.GetOrganization(collection.OrganizationId) == null && await _currentContext.ProviderUserForOrgAsync(collection.OrganizationId))) { diff --git a/src/Api/Public/Controllers/CollectionsController.cs b/src/Api/Public/Controllers/CollectionsController.cs index b16eb1a418..d4e0932caa 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.OrganizationFeatures.OrganizationCollections.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; @@ -14,18 +15,18 @@ namespace Bit.Api.Public.Controllers; public class CollectionsController : Controller { private readonly ICollectionRepository _collectionRepository; - private readonly ICollectionService _collectionService; + private readonly IUpdateCollectionCommand _updateCollectionCommand; private readonly ICurrentContext _currentContext; private readonly IApplicationCacheService _applicationCacheService; public CollectionsController( ICollectionRepository collectionRepository, - ICollectionService collectionService, + IUpdateCollectionCommand updateCollectionCommand, ICurrentContext currentContext, IApplicationCacheService applicationCacheService) { _collectionRepository = collectionRepository; - _collectionService = collectionService; + _updateCollectionCommand = updateCollectionCommand; _currentContext = currentContext; _applicationCacheService = applicationCacheService; } @@ -93,7 +94,7 @@ public class CollectionsController : Controller } var updatedCollection = model.ToCollection(existingCollection); var associations = model.Groups?.Select(c => c.ToCollectionAccessSelection()).ToList(); - await _collectionService.SaveAsync(updatedCollection, associations); + await _updateCollectionCommand.UpdateAsync(updatedCollection, associations, null); var response = new CollectionResponseModel(updatedCollection, associations); return new JsonResult(response); } diff --git a/src/Core/OrganizationFeatures/OrganizationCollections/CreateCollectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationCollections/CreateCollectionCommand.cs new file mode 100644 index 0000000000..1cec2f5cc4 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationCollections/CreateCollectionCommand.cs @@ -0,0 +1,70 @@ +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; + +namespace Bit.Core.OrganizationFeatures.OrganizationCollections; + +public class CreateCollectionCommand : ICreateCollectionCommand +{ + private readonly IEventService _eventService; + private readonly IOrganizationRepository _organizationRepository; + private readonly ICollectionRepository _collectionRepository; + + public CreateCollectionCommand( + IEventService eventService, + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository) + { + _eventService = eventService; + _organizationRepository = organizationRepository; + _collectionRepository = collectionRepository; + } + + public async Task CreateAsync(Collection collection, IEnumerable groups = null, + IEnumerable users = null) + { + var org = await _organizationRepository.GetByIdAsync(collection.OrganizationId); + if (org == null) + { + throw new BadRequestException("Organization not found"); + } + + var groupsList = groups?.ToList(); + var usersList = users?.ToList(); + + // Cannot use Manage with ReadOnly/HidePasswords permissions + var invalidAssociations = groupsList?.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords)); + if (invalidAssociations?.Any() ?? false) + { + throw new BadRequestException("The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true."); + } + + // A collection should always have someone with Can Manage permissions + var groupHasManageAccess = groupsList?.Any(g => g.Manage) ?? false; + var userHasManageAccess = usersList?.Any(u => u.Manage) ?? false; + if (!groupHasManageAccess && !userHasManageAccess && !org.AllowAdminAccessToAllCollectionItems) + { + throw new BadRequestException( + "At least one member or group must have can manage permission."); + } + + // Check max collections limit + if (org.MaxCollections.HasValue) + { + var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(org.Id); + if (org.MaxCollections.Value <= collectionCount) + { + throw new BadRequestException("You have reached the maximum number of collections " + + $"({org.MaxCollections.Value}) for this organization."); + } + } + + await _collectionRepository.CreateAsync(collection, org.UseGroups ? groupsList : null, usersList); + await _eventService.LogCollectionEventAsync(collection, Enums.EventType.Collection_Created); + + return collection; + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/ICreateCollectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/ICreateCollectionCommand.cs new file mode 100644 index 0000000000..b73afb4d1e --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/ICreateCollectionCommand.cs @@ -0,0 +1,17 @@ +using Bit.Core.Entities; +using Bit.Core.Models.Data; + +namespace Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; + +public interface ICreateCollectionCommand +{ + /// + /// Creates a new collection. + /// + /// The collection to create. + /// (Optional) The groups that will have access to the collection. + /// (Optional) The users that will have access to the collection. + /// The created collection. + Task CreateAsync(Collection collection, IEnumerable groups = null, + IEnumerable users = null); +} diff --git a/src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/IUpdateCollectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/IUpdateCollectionCommand.cs new file mode 100644 index 0000000000..94d4d1d1f8 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/IUpdateCollectionCommand.cs @@ -0,0 +1,17 @@ +using Bit.Core.Entities; +using Bit.Core.Models.Data; + +namespace Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; + +public interface IUpdateCollectionCommand +{ + /// + /// Updates a collection. + /// + /// The collection to update. + /// (Optional) The groups that will have access to the collection. + /// (Optional) The users that will have access to the collection. + /// The updated collection. + Task UpdateAsync(Collection collection, IEnumerable groups = null, + IEnumerable users = null); +} diff --git a/src/Core/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommand.cs new file mode 100644 index 0000000000..3985b6a919 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommand.cs @@ -0,0 +1,59 @@ +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; + +namespace Bit.Core.OrganizationFeatures.OrganizationCollections; + +public class UpdateCollectionCommand : IUpdateCollectionCommand +{ + private readonly IEventService _eventService; + private readonly IOrganizationRepository _organizationRepository; + private readonly ICollectionRepository _collectionRepository; + + public UpdateCollectionCommand( + IEventService eventService, + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository) + { + _eventService = eventService; + _organizationRepository = organizationRepository; + _collectionRepository = collectionRepository; + } + + public async Task UpdateAsync(Collection collection, IEnumerable groups = null, + IEnumerable users = null) + { + var org = await _organizationRepository.GetByIdAsync(collection.OrganizationId); + if (org == null) + { + throw new BadRequestException("Organization not found"); + } + + var groupsList = groups?.ToList(); + var usersList = users?.ToList(); + + // Cannot use Manage with ReadOnly/HidePasswords permissions + var invalidAssociations = groupsList?.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords)); + if (invalidAssociations?.Any() ?? false) + { + throw new BadRequestException("The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true."); + } + + // A collection should always have someone with Can Manage permissions + var groupHasManageAccess = groupsList?.Any(g => g.Manage) ?? false; + var userHasManageAccess = usersList?.Any(u => u.Manage) ?? false; + if (!groupHasManageAccess && !userHasManageAccess && !org.AllowAdminAccessToAllCollectionItems) + { + throw new BadRequestException( + "At least one member or group must have can manage permission."); + } + + await _collectionRepository.ReplaceAsync(collection, org.UseGroups ? groupsList : null, usersList); + await _eventService.LogCollectionEventAsync(collection, Enums.EventType.Collection_Updated); + + return collection; + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 2bc05017d5..8a02ee68d8 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -143,6 +143,8 @@ public static class OrganizationServiceCollectionExtensions public static void AddOrganizationCollectionCommands(this IServiceCollection services) { + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); } diff --git a/src/Core/Services/ICollectionService.cs b/src/Core/Services/ICollectionService.cs index c116e5f076..101f3ea23b 100644 --- a/src/Core/Services/ICollectionService.cs +++ b/src/Core/Services/ICollectionService.cs @@ -1,10 +1,8 @@ using Bit.Core.Entities; -using Bit.Core.Models.Data; namespace Bit.Core.Services; public interface ICollectionService { - Task SaveAsync(Collection collection, IEnumerable groups = null, IEnumerable users = null); Task DeleteUserAsync(Collection collection, Guid organizationUserId); } diff --git a/src/Core/Services/Implementations/CollectionService.cs b/src/Core/Services/Implementations/CollectionService.cs index 852f2da073..2a3f8c42dc 100644 --- a/src/Core/Services/Implementations/CollectionService.cs +++ b/src/Core/Services/Implementations/CollectionService.cs @@ -2,7 +2,6 @@ using Bit.Core.Entities; using Bit.Core.Exceptions; -using Bit.Core.Models.Data; using Bit.Core.Repositories; namespace Bit.Core.Services; @@ -10,71 +9,20 @@ namespace Bit.Core.Services; public class CollectionService : ICollectionService { private readonly IEventService _eventService; - private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly ICollectionRepository _collectionRepository; public CollectionService( IEventService eventService, - IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, ICollectionRepository collectionRepository) { _eventService = eventService; - _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; _collectionRepository = collectionRepository; } - public async Task SaveAsync(Collection collection, IEnumerable? groups = null, - IEnumerable? users = null) - { - var org = await _organizationRepository.GetByIdAsync(collection.OrganizationId); - if (org == null) - { - throw new BadRequestException("Organization not found"); - } - var groupsList = groups?.ToList(); - var usersList = users?.ToList(); - - // Cannot use Manage with ReadOnly/HidePasswords permissions - var invalidAssociations = groupsList?.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords)); - if (invalidAssociations?.Any() ?? false) - { - throw new BadRequestException("The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true."); - } - - // A collection should always have someone with Can Manage permissions - var groupHasManageAccess = groupsList?.Any(g => g.Manage) ?? false; - var userHasManageAccess = usersList?.Any(u => u.Manage) ?? false; - if (!groupHasManageAccess && !userHasManageAccess && !org.AllowAdminAccessToAllCollectionItems) - { - throw new BadRequestException( - "At least one member or group must have can manage permission."); - } - - if (collection.Id == default(Guid)) - { - if (org.MaxCollections.HasValue) - { - var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(org.Id); - if (org.MaxCollections.Value <= collectionCount) - { - throw new BadRequestException("You have reached the maximum number of collections " + - $"({org.MaxCollections.Value}) for this organization."); - } - } - - await _collectionRepository.CreateAsync(collection, org.UseGroups ? groupsList : null, usersList); - await _eventService.LogCollectionEventAsync(collection, Enums.EventType.Collection_Created); - } - else - { - await _collectionRepository.ReplaceAsync(collection, org.UseGroups ? groupsList : null, usersList); - await _eventService.LogCollectionEventAsync(collection, Enums.EventType.Collection_Updated); - } - } public async Task DeleteUserAsync(Collection collection, Guid organizationUserId) { diff --git a/test/Api.Test/Controllers/CollectionsControllerTests.cs b/test/Api.Test/Controllers/CollectionsControllerTests.cs index 09e93d15f7..bdcf6bc74e 100644 --- a/test/Api.Test/Controllers/CollectionsControllerTests.cs +++ b/test/Api.Test/Controllers/CollectionsControllerTests.cs @@ -9,7 +9,6 @@ using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Authorization; @@ -38,9 +37,11 @@ public class CollectionsControllerTests _ = await sutProvider.Sut.Post(organization.Id, collectionRequest); - await sutProvider.GetDependency() + await sutProvider.GetDependency() .Received(1) - .SaveAsync(Arg.Any(), Arg.Any>(), + .CreateAsync(Arg.Is(c => + c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId && c.OrganizationId == organization.Id), + Arg.Any>(), Arg.Any>()); } @@ -64,9 +65,9 @@ public class CollectionsControllerTests _ = await sutProvider.Sut.Put(collection.OrganizationId, collection.Id, collectionRequest); - await sutProvider.GetDependency() + await sutProvider.GetDependency() .Received(1) - .SaveAsync(ExpectedCollection(), Arg.Any>(), + .UpdateAsync(ExpectedCollection(), Arg.Any>(), Arg.Any>()); } diff --git a/test/Core.Test/OrganizationFeatures/OrganizationCollections/CreateCollectionCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationCollections/CreateCollectionCommandTests.cs new file mode 100644 index 0000000000..d180bad432 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationCollections/CreateCollectionCommandTests.cs @@ -0,0 +1,202 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.OrganizationFeatures.OrganizationCollections; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Test.AutoFixture; +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.OrganizationFeatures.OrganizationCollections; + +[SutProviderCustomize] +[OrganizationCustomize] +public class CreateCollectionCommandTests +{ + [Theory, BitAutoData] + public async Task CreateAsync_WithoutGroupsAndUsers_CreatesCollection( + Organization organization, Collection collection, + SutProvider sutProvider) + { + collection.Id = default; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + var utcNow = DateTime.UtcNow; + + await sutProvider.Sut.CreateAsync(collection, null, null); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync( + collection, + Arg.Is>(l => l == null), + Arg.Is>(l => l == null)); + await sutProvider.GetDependency() + .Received(1) + .LogCollectionEventAsync(collection, EventType.Collection_Created); + Assert.True(collection.CreationDate - utcNow < TimeSpan.FromSeconds(1)); + Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); + } + + [Theory, BitAutoData] + public async Task CreateAsync_WithGroupsAndUsers_CreatesCollectionWithGroupsAndUsers( + Organization organization, Collection collection, + [CollectionAccessSelectionCustomize(true)] IEnumerable groups, + IEnumerable users, + SutProvider sutProvider) + { + collection.Id = default; + organization.UseGroups = true; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + var utcNow = DateTime.UtcNow; + + await sutProvider.Sut.CreateAsync(collection, groups, users); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync( + collection, + Arg.Is>(l => l.Any(i => i.Manage == true)), + Arg.Any>()); + await sutProvider.GetDependency() + .Received(1) + .LogCollectionEventAsync(collection, EventType.Collection_Created); + Assert.True(collection.CreationDate - utcNow < TimeSpan.FromSeconds(1)); + Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); + } + + [Theory, BitAutoData] + public async Task CreateAsync_WithOrganizationUseGroupDisabled_CreatesCollectionWithoutGroups( + Organization organization, Collection collection, + [CollectionAccessSelectionCustomize] IEnumerable groups, + [CollectionAccessSelectionCustomize(true)] IEnumerable users, + SutProvider sutProvider) + { + collection.Id = default; + organization.UseGroups = false; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + var utcNow = DateTime.UtcNow; + + await sutProvider.Sut.CreateAsync(collection, groups, users); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync( + collection, + Arg.Is>(l => l == null), + Arg.Is>(l => l.Any(i => i.Manage == true))); + await sutProvider.GetDependency() + .Received(1) + .LogCollectionEventAsync(collection, EventType.Collection_Created); + Assert.True(collection.CreationDate - utcNow < TimeSpan.FromSeconds(1)); + Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); + } + + [Theory, BitAutoData] + public async Task CreateAsync_WithNonExistingOrganizationId_ThrowsBadRequest( + Collection collection, SutProvider sutProvider) + { + collection.Id = default; + var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(collection)); + Assert.Contains("Organization not found", ex.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .CreateAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .CreateAsync(default, default, default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogCollectionEventAsync(default, default); + } + + [Theory, BitAutoData] + public async Task CreateAsync_WithoutManageAccess_ThrowsBadRequest( + Organization organization, Collection collection, + [CollectionAccessSelectionCustomize] IEnumerable users, + SutProvider sutProvider) + { + collection.Id = default; + organization.AllowAdminAccessToAllCollectionItems = false; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(collection, null, users)); + Assert.Contains("At least one member or group must have can manage permission.", ex.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .CreateAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .CreateAsync(default, default, default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogCollectionEventAsync(default, default); + } + + [Theory, BitAutoData] + public async Task CreateAsync_WithExceedsOrganizationMaxCollections_ThrowsBadRequest( + Organization organization, Collection collection, + [CollectionAccessSelectionCustomize(true)] IEnumerable users, + SutProvider sutProvider) + { + collection.Id = default; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + sutProvider.GetDependency() + .GetCountByOrganizationIdAsync(organization.Id) + .Returns(organization.MaxCollections.Value); + + var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(collection, null, users)); + Assert.Equal($@"You have reached the maximum number of collections ({organization.MaxCollections.Value}) for this organization.", ex.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .CreateAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .CreateAsync(default, default, default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogCollectionEventAsync(default, default); + } + + [Theory, BitAutoData] + public async Task CreateAsync_WithInvalidManageAssociations_ThrowsBadRequest( + Organization organization, Collection collection, SutProvider sutProvider) + { + collection.Id = default; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + var invalidGroups = new List + { + new() { Id = Guid.NewGuid(), Manage = true, ReadOnly = true } + }; + + var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(collection, invalidGroups, null)); + Assert.Contains("The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.", 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/UpdateCollectionCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommandTests.cs new file mode 100644 index 0000000000..5147157750 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommandTests.cs @@ -0,0 +1,169 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.OrganizationFeatures.OrganizationCollections; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Test.AutoFixture; +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.OrganizationFeatures.OrganizationCollections; + +[SutProviderCustomize] +[OrganizationCustomize] +public class UpdateCollectionCommandTests +{ + [Theory, BitAutoData] + public async Task UpdateAsync_WithoutGroupsAndUsers_ReplacesCollection( + Organization organization, Collection collection, SutProvider sutProvider) + { + var creationDate = collection.CreationDate; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + var utcNow = DateTime.UtcNow; + + await sutProvider.Sut.UpdateAsync(collection, null, null); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync( + collection, + Arg.Is>(l => l == null), + Arg.Is>(l => l == null)); + await sutProvider.GetDependency() + .Received(1) + .LogCollectionEventAsync(collection, EventType.Collection_Updated); + Assert.Equal(collection.CreationDate, creationDate); + Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_WithGroupsAndUsers_ReplacesCollectionWithGroupsAndUsers( + Organization organization, Collection collection, + [CollectionAccessSelectionCustomize(true)] IEnumerable groups, + IEnumerable users, + SutProvider sutProvider) + { + var creationDate = collection.CreationDate; + organization.UseGroups = true; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + var utcNow = DateTime.UtcNow; + + await sutProvider.Sut.UpdateAsync(collection, groups, users); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync( + collection, + Arg.Is>(l => l.Any(i => i.Manage == true)), + Arg.Any>()); + await sutProvider.GetDependency() + .Received(1) + .LogCollectionEventAsync(collection, EventType.Collection_Updated); + Assert.Equal(collection.CreationDate, creationDate); + Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_WithOrganizationUseGroupDisabled_ReplacesCollectionWithoutGroups( + Organization organization, Collection collection, + [CollectionAccessSelectionCustomize] IEnumerable groups, + [CollectionAccessSelectionCustomize(true)] IEnumerable users, + SutProvider sutProvider) + { + var creationDate = collection.CreationDate; + organization.UseGroups = false; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + var utcNow = DateTime.UtcNow; + + await sutProvider.Sut.UpdateAsync(collection, groups, users); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync( + collection, + Arg.Is>(l => l == null), + Arg.Is>(l => l.Any(i => i.Manage == true))); + await sutProvider.GetDependency() + .Received(1) + .LogCollectionEventAsync(collection, EventType.Collection_Updated); + Assert.Equal(collection.CreationDate, creationDate); + Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_WithNonExistingOrganizationId_ThrowsBadRequest( + Collection collection, SutProvider sutProvider) + { + var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(collection)); + Assert.Contains("Organization not found", ex.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default, default, default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogCollectionEventAsync(default, default); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_WithoutManageAccess_ThrowsBadRequest( + Organization organization, Collection collection, + [CollectionAccessSelectionCustomize] IEnumerable users, + SutProvider sutProvider) + { + organization.AllowAdminAccessToAllCollectionItems = false; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(collection, null, users)); + Assert.Contains("At least one member or group must have can manage permission.", ex.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default, default, default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogCollectionEventAsync(default, default); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_WithInvalidManageAssociations_ThrowsBadRequest( + Organization organization, Collection collection, SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + + var invalidGroups = new List + { + new() { Id = Guid.NewGuid(), Manage = true, HidePasswords = true } + }; + + var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(collection, invalidGroups, null)); + Assert.Contains("The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.", 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 6d788deb05..2f99467700 100644 --- a/test/Core.Test/Services/CollectionServiceTests.cs +++ b/test/Core.Test/Services/CollectionServiceTests.cs @@ -2,10 +2,8 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Test.AutoFixture; using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -18,135 +16,12 @@ namespace Bit.Core.Test.Services; [OrganizationCustomize] public class CollectionServiceTest { - [Theory, BitAutoData] - public async Task SaveAsync_DefaultIdWithUsers_CreatesCollectionInTheRepository(Collection collection, Organization organization, [CollectionAccessSelectionCustomize(true)] IEnumerable users, SutProvider sutProvider) - { - collection.Id = default; - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - var utcNow = DateTime.UtcNow; - - await sutProvider.Sut.SaveAsync(collection, null, users); - - await sutProvider.GetDependency().Received() - .CreateAsync(collection, Arg.Is>(l => l == null), - Arg.Is>(l => l.Any(i => i.Manage == true))); - await sutProvider.GetDependency().Received() - .LogCollectionEventAsync(collection, EventType.Collection_Created); - Assert.True(collection.CreationDate - utcNow < TimeSpan.FromSeconds(1)); - Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); - } - - [Theory, BitAutoData] - public async Task SaveAsync_DefaultIdWithGroupsAndUsers_CreateCollectionWithGroupsAndUsersInRepository(Collection collection, - [CollectionAccessSelectionCustomize(true)] IEnumerable groups, IEnumerable users, Organization organization, SutProvider sutProvider) - { - collection.Id = default; - organization.UseGroups = true; - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - var utcNow = DateTime.UtcNow; - - await sutProvider.Sut.SaveAsync(collection, groups, users); - - await sutProvider.GetDependency().Received() - .CreateAsync(collection, Arg.Is>(l => l.Any(i => i.Manage == true)), - Arg.Any>()); - await sutProvider.GetDependency().Received() - .LogCollectionEventAsync(collection, EventType.Collection_Created); - Assert.True(collection.CreationDate - utcNow < TimeSpan.FromSeconds(1)); - Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); - } - - [Theory, BitAutoData] - public async Task SaveAsync_NonDefaultId_ReplacesCollectionInRepository(Collection collection, Organization organization, [CollectionAccessSelectionCustomize(true)] IEnumerable users, SutProvider sutProvider) - { - var creationDate = collection.CreationDate; - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - var utcNow = DateTime.UtcNow; - - await sutProvider.Sut.SaveAsync(collection, null, users); - - await sutProvider.GetDependency().Received().ReplaceAsync(collection, - Arg.Is>(l => l == null), - Arg.Is>(l => l.Any(i => i.Manage == true))); - await sutProvider.GetDependency().Received() - .LogCollectionEventAsync(collection, EventType.Collection_Updated); - Assert.Equal(collection.CreationDate, creationDate); - Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); - } - - [Theory, BitAutoData] - public async Task SaveAsync_OrganizationNotUseGroup_CreateCollectionWithoutGroupsInRepository(Collection collection, - [CollectionAccessSelectionCustomize] IEnumerable groups, [CollectionAccessSelectionCustomize(true)] IEnumerable users, - Organization organization, SutProvider sutProvider) - { - collection.Id = default; - organization.UseGroups = false; - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - var utcNow = DateTime.UtcNow; - - await sutProvider.Sut.SaveAsync(collection, groups, users); - - await sutProvider.GetDependency().Received().CreateAsync(collection, - Arg.Is>(l => l == null), - Arg.Is>(l => l.Any(i => i.Manage == true))); - await sutProvider.GetDependency().Received() - .LogCollectionEventAsync(collection, EventType.Collection_Created); - Assert.True(collection.CreationDate - utcNow < TimeSpan.FromSeconds(1)); - Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); - } - - [Theory, BitAutoData] - public async Task SaveAsync_NonExistingOrganizationId_ThrowsBadRequest(Collection collection, SutProvider sutProvider) - { - var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.SaveAsync(collection)); - Assert.Contains("Organization not found", ex.Message); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default, default, default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().LogCollectionEventAsync(default, default); - } - - [Theory, BitAutoData] - public async Task SaveAsync_NoManageAccess_ThrowsBadRequest(Collection collection, Organization organization, - [CollectionAccessSelectionCustomize] IEnumerable users, SutProvider sutProvider) - { - collection.Id = default; - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - organization.AllowAdminAccessToAllCollectionItems = false; - - var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.SaveAsync(collection, null, users)); - Assert.Contains("At least one member or group must have can manage permission.", ex.Message); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default, default, default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().LogCollectionEventAsync(default, default); - } - - [Theory, BitAutoData] - public async Task SaveAsync_ExceedsOrganizationMaxCollections_ThrowsBadRequest(Collection collection, - Organization organization, [CollectionAccessSelectionCustomize(true)] IEnumerable users, - SutProvider sutProvider) - { - collection.Id = default; - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - sutProvider.GetDependency().GetCountByOrganizationIdAsync(organization.Id) - .Returns(organization.MaxCollections.Value); - - var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.SaveAsync(collection, null, users)); - Assert.Equal($@"You have reached the maximum number of collections ({organization.MaxCollections.Value}) for this organization.", ex.Message); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default, default, default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().LogCollectionEventAsync(default, default); - } - [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(organization.Id).Returns(organization); sutProvider.GetDependency().GetByIdAsync(organizationUser.Id) .Returns(organizationUser); @@ -162,7 +37,6 @@ public class CollectionServiceTest OrganizationUser organizationUser, SutProvider sutProvider) { collection.OrganizationId = organization.Id; - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency().GetByIdAsync(organizationUser.Id) .Returns(organizationUser); From cc07f13c7cf87fe668f8f048e2a6d266ae3eacfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:21:48 +0100 Subject: [PATCH 20/47] [PM-22442] Remove unused user management methods from CollectionsController (#5969) --- src/Api/Controllers/CollectionsController.cs | 27 -------------------- 1 file changed, 27 deletions(-) diff --git a/src/Api/Controllers/CollectionsController.cs b/src/Api/Controllers/CollectionsController.cs index e18999c1e0..c8a12b9c22 100644 --- a/src/Api/Controllers/CollectionsController.cs +++ b/src/Api/Controllers/CollectionsController.cs @@ -198,19 +198,6 @@ public class CollectionsController : Controller return new CollectionAccessDetailsResponseModel(collectionWithPermissions); } - [HttpPut("{id}/users")] - public async Task PutUsers(Guid orgId, Guid id, [FromBody] IEnumerable model) - { - var collection = await _collectionRepository.GetByIdAsync(id); - var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyUserAccess)).Succeeded; - if (!authorized) - { - throw new NotFoundException(); - } - - await _collectionRepository.UpdateUsersAsync(collection.Id, model?.Select(g => g.ToSelectionReadOnly())); - } - [HttpPost("bulk-access")] public async Task PostBulkCollectionAccess(Guid orgId, [FromBody] BulkCollectionAccessRequestModel model) { @@ -261,18 +248,4 @@ public class CollectionsController : Controller await _deleteCollectionCommand.DeleteManyAsync(collections); } - - [HttpDelete("{id}/user/{orgUserId}")] - [HttpPost("{id}/delete-user/{orgUserId}")] - public async Task DeleteUser(Guid orgId, Guid id, Guid orgUserId) - { - var collection = await _collectionRepository.GetByIdAsync(id); - var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyUserAccess)).Succeeded; - if (!authorized) - { - throw new NotFoundException(); - } - - await _collectionService.DeleteUserAsync(collection, orgUserId); - } } From 494c41e3b1073769ceb42e3935f660dfd5085891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:33:09 +0100 Subject: [PATCH 21/47] [PM-15160] Create ResellerClientOrganizationSignUpCommand (#5981) * Implement ResellerClientOrganizationSignUpCommand for signing up reseller client organizations with email invitations and error handling * Refactor ProvidersController to replace IOrganizationService with IResellerClientOrganizationSignUpCommand for organization sign-up process * Remove CreatePendingOrganization method from IOrganizationService and its implementation in OrganizationService * Add IResellerClientOrganizationSignUpCommand to service collection for organization sign-up * Add comment to clarify organization deletion process in ResellerClientOrganizationSignUpCommand --- .../Controllers/ProvidersController.cs | 12 +- ...ResellerClientOrganizationSignUpCommand.cs | 130 ++++++++++++ .../Services/IOrganizationService.cs | 4 +- .../Implementations/OrganizationService.cs | 26 +-- ...OrganizationServiceCollectionExtensions.cs | 1 + ...lerClientOrganizationSignUpCommandTests.cs | 185 ++++++++++++++++++ 6 files changed, 323 insertions(+), 35 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/ResellerClientOrganizationSignUpCommand.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ResellerClientOrganizationSignUpCommandTests.cs diff --git a/src/Admin/AdminConsole/Controllers/ProvidersController.cs b/src/Admin/AdminConsole/Controllers/ProvidersController.cs index b4abf81ee2..7f11b65d9e 100644 --- a/src/Admin/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Admin/AdminConsole/Controllers/ProvidersController.cs @@ -6,6 +6,7 @@ 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; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; @@ -34,14 +35,13 @@ namespace Bit.Admin.AdminConsole.Controllers; public class ProvidersController : Controller { private readonly IOrganizationRepository _organizationRepository; - private readonly IOrganizationService _organizationService; + private readonly IResellerClientOrganizationSignUpCommand _resellerClientOrganizationSignUpCommand; private readonly IProviderRepository _providerRepository; private readonly IProviderUserRepository _providerUserRepository; private readonly IProviderOrganizationRepository _providerOrganizationRepository; private readonly GlobalSettings _globalSettings; private readonly IApplicationCacheService _applicationCacheService; private readonly IProviderService _providerService; - private readonly IUserService _userService; private readonly ICreateProviderCommand _createProviderCommand; private readonly IFeatureService _featureService; private readonly IProviderPlanRepository _providerPlanRepository; @@ -54,14 +54,13 @@ public class ProvidersController : Controller public ProvidersController( IOrganizationRepository organizationRepository, - IOrganizationService organizationService, + IResellerClientOrganizationSignUpCommand resellerClientOrganizationSignUpCommand, IProviderRepository providerRepository, IProviderUserRepository providerUserRepository, IProviderOrganizationRepository providerOrganizationRepository, IProviderService providerService, GlobalSettings globalSettings, IApplicationCacheService applicationCacheService, - IUserService userService, ICreateProviderCommand createProviderCommand, IFeatureService featureService, IProviderPlanRepository providerPlanRepository, @@ -71,14 +70,13 @@ public class ProvidersController : Controller IStripeAdapter stripeAdapter) { _organizationRepository = organizationRepository; - _organizationService = organizationService; + _resellerClientOrganizationSignUpCommand = resellerClientOrganizationSignUpCommand; _providerRepository = providerRepository; _providerUserRepository = providerUserRepository; _providerOrganizationRepository = providerOrganizationRepository; _providerService = providerService; _globalSettings = globalSettings; _applicationCacheService = applicationCacheService; - _userService = userService; _createProviderCommand = createProviderCommand; _featureService = featureService; _providerPlanRepository = providerPlanRepository; @@ -459,7 +457,7 @@ public class ProvidersController : Controller } var organization = model.CreateOrganization(provider); - await _organizationService.CreatePendingOrganization(organization, model.Owners, User, _userService, model.SalesAssistedTrialStarted); + await _resellerClientOrganizationSignUpCommand.SignUpResellerClientAsync(organization, model.Owners); await _providerService.AddOrganization(providerId, organization.Id, null); return RedirectToAction("Edit", "Providers", new { id = providerId }); diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ResellerClientOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ResellerClientOrganizationSignUpCommand.cs new file mode 100644 index 0000000000..446d7339ca --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ResellerClientOrganizationSignUpCommand.cs @@ -0,0 +1,130 @@ +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.Repositories; +using Bit.Core.Services; +using Bit.Core.Utilities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; + +public record ResellerClientOrganizationSignUpResponse( + Organization Organization, + OrganizationUser OwnerOrganizationUser); + +/// +/// Command for signing up reseller client organizations in a pending state. +/// +public interface IResellerClientOrganizationSignUpCommand +{ + /// + /// Sign up a reseller client organization. The organization will be created in a pending state + /// (disabled and with Pending status) and the owner will be invited via email. The organization + /// will become active once the owner accepts the invitation. + /// + /// The organization to create. + /// The email of the organization owner who will be invited. + /// A response containing the created pending organization and invited owner user. + Task SignUpResellerClientAsync( + Organization organization, + string ownerEmail); +} + +public class ResellerClientOrganizationSignUpCommand : IResellerClientOrganizationSignUpCommand +{ + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; + private readonly IApplicationCacheService _applicationCacheService; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IEventService _eventService; + private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand; + private readonly IPaymentService _paymentService; + + public ResellerClientOrganizationSignUpCommand( + IOrganizationRepository organizationRepository, + IOrganizationApiKeyRepository organizationApiKeyRepository, + IApplicationCacheService applicationCacheService, + IOrganizationUserRepository organizationUserRepository, + IEventService eventService, + ISendOrganizationInvitesCommand sendOrganizationInvitesCommand, + IPaymentService paymentService) + { + _organizationRepository = organizationRepository; + _organizationApiKeyRepository = organizationApiKeyRepository; + _applicationCacheService = applicationCacheService; + _organizationUserRepository = organizationUserRepository; + _eventService = eventService; + _sendOrganizationInvitesCommand = sendOrganizationInvitesCommand; + _paymentService = paymentService; + } + + public async Task SignUpResellerClientAsync( + Organization organization, + string ownerEmail) + { + try + { + var createdOrganization = await CreateOrganizationAsync(organization); + var ownerOrganizationUser = await CreateAndInviteOwnerAsync(createdOrganization, ownerEmail); + + await _eventService.LogOrganizationUserEventAsync(ownerOrganizationUser, EventType.OrganizationUser_Invited); + + return new ResellerClientOrganizationSignUpResponse(organization, ownerOrganizationUser); + } + catch + { + await _paymentService.CancelAndRecoverChargesAsync(organization); + + if (organization.Id != default) + { + // Deletes the organization and all related data, including its owner user + await _organizationRepository.DeleteAsync(organization); + await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id); + } + + throw; + } + } + + private async Task CreateOrganizationAsync(Organization organization) + { + organization.Id = CoreHelpers.GenerateComb(); + organization.Enabled = false; + organization.Status = OrganizationStatusType.Pending; + + 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); + + return organization; + } + + private async Task CreateAndInviteOwnerAsync(Organization organization, string ownerEmail) + { + var ownerOrganizationUser = new OrganizationUser + { + OrganizationId = organization.Id, + UserId = null, + Email = ownerEmail, + Key = null, + Type = OrganizationUserType.Owner, + Status = OrganizationUserStatusType.Invited, + }; + + await _organizationUserRepository.CreateAsync(ownerOrganizationUser); + + await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest( + users: [ownerOrganizationUser], + organization: organization, + initOrganization: true)); + + return ownerOrganizationUser; + } +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 5fe68bd22e..feae561a19 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -1,5 +1,4 @@ -using System.Security.Claims; -using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.Auth.Enums; using Bit.Core.Entities; @@ -42,7 +41,6 @@ public interface IOrganizationService Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser); Task>> RevokeUsersAsync(Guid organizationId, IEnumerable organizationUserIds, Guid? revokingUserId); - Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted); 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 4d709bb7cf..d5320b5110 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -1,5 +1,4 @@ -using System.Security.Claims; -using System.Text.Json; +using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums.Provider; @@ -1705,27 +1704,4 @@ public class OrganizationService : IOrganizationService return status; } - - public async Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted) - { - organization.Id = CoreHelpers.GenerateComb(); - organization.Enabled = false; - organization.Status = OrganizationStatusType.Pending; - - await SignUpAsync(organization, default, null, null, true); - - var ownerOrganizationUser = new OrganizationUser - { - OrganizationId = organization.Id, - UserId = null, - Email = ownerEmail, - Key = null, - Type = OrganizationUserType.Owner, - Status = OrganizationUserStatusType.Invited, - }; - await _organizationUserRepository.CreateAsync(ownerOrganizationUser); - - await SendInviteAsync(ownerOrganizationUser, organization, true); - await _eventService.LogOrganizationUserEventAsync(ownerOrganizationUser, EventType.OrganizationUser_Invited); - } } diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 8a02ee68d8..ef78e966f6 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -73,6 +73,7 @@ public static class OrganizationServiceCollectionExtensions { services.AddScoped(); services.AddScoped(); + services.AddScoped(); } private static void AddOrganizationDeleteCommands(this IServiceCollection services) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ResellerClientOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ResellerClientOrganizationSignUpCommandTests.cs new file mode 100644 index 0000000000..55e5698ad4 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ResellerClientOrganizationSignUpCommandTests.cs @@ -0,0 +1,185 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +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.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations.OrganizationSignUp; + +[SutProviderCustomize] +public class ResellerClientOrganizationSignUpCommandTests +{ + [Theory] + [BitAutoData] + public async Task SignUpResellerClientAsync_WithValidParameters_CreatesOrganizationSuccessfully( + Organization organization, + string ownerEmail, + SutProvider sutProvider) + { + var result = await sutProvider.Sut.SignUpResellerClientAsync(organization, ownerEmail); + + Assert.NotNull(result.Organization); + Assert.False(result.Organization.Enabled); + Assert.Equal(OrganizationStatusType.Pending, result.Organization.Status); + Assert.NotNull(result.OwnerOrganizationUser); + Assert.Equal(ownerEmail, result.OwnerOrganizationUser.Email); + Assert.Equal(OrganizationUserType.Owner, result.OwnerOrganizationUser.Type); + Assert.Equal(OrganizationUserStatusType.Invited, result.OwnerOrganizationUser.Status); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync( + Arg.Is(o => + o.Id != default && + o.Name == organization.Name && + o.Enabled == false && + o.Status == OrganizationStatusType.Pending + ) + ); + await sutProvider.GetDependency() + .Received(1) + .CreateAsync( + Arg.Is(k => + k.OrganizationId == result.Organization.Id && + k.Type == OrganizationApiKeyType.Default && + !string.IsNullOrEmpty(k.ApiKey) + ) + ); + await sutProvider.GetDependency() + .Received(1) + .UpsertOrganizationAbilityAsync(Arg.Is(o => o.Id == result.Organization.Id)); + await sutProvider.GetDependency() + .Received(1) + .CreateAsync( + Arg.Is(u => + u.OrganizationId == result.Organization.Id && + u.Email == ownerEmail && + u.Type == OrganizationUserType.Owner && + u.Status == OrganizationUserStatusType.Invited && + u.UserId == null + ) + ); + await sutProvider.GetDependency() + .Received(1) + .SendInvitesAsync( + Arg.Is(r => + r.Users.Count() == 1 && + r.Users.First().Email == ownerEmail && + r.Organization.Id == result.Organization.Id && + r.InitOrganization == true + ) + ); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync( + Arg.Is(u => u.Email == ownerEmail), + EventType.OrganizationUser_Invited + ); + } + + [Theory] + [BitAutoData] + public async Task SignUpResellerClientAsync_WhenOrganizationRepositoryThrows_PerformsCleanup( + Organization organization, + string ownerEmail, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .When(x => x.CreateAsync(Arg.Any())) + .Do(_ => throw new Exception()); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpResellerClientAsync(organization, ownerEmail)); + + await AssertCleanupIsPerformed(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task SignUpResellerClientAsync_WhenOrganizationUserCreationFails_PerformsCleanup( + Organization organization, + string ownerEmail, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .When(x => x.CreateAsync(Arg.Any())) + .Do(_ => throw new Exception()); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpResellerClientAsync(organization, ownerEmail)); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(Arg.Any()); + await AssertCleanupIsPerformed(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task SignUpResellerClientAsync_WhenInvitationSendingFails_PerformsCleanup( + Organization organization, + string ownerEmail, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .When(x => x.SendInvitesAsync(Arg.Any())) + .Do(_ => throw new Exception()); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpResellerClientAsync(organization, ownerEmail)); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(Arg.Any()); + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(Arg.Any()); + await AssertCleanupIsPerformed(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task SignUpResellerClientAsync_WhenEventLoggingFails_PerformsCleanup( + Organization organization, + string ownerEmail, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .When(x => x.LogOrganizationUserEventAsync(Arg.Any(), Arg.Any())) + .Do(_ => throw new Exception()); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpResellerClientAsync(organization, ownerEmail)); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(Arg.Any()); + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(Arg.Any()); + await sutProvider.GetDependency() + .Received(1) + .SendInvitesAsync(Arg.Any()); + await AssertCleanupIsPerformed(sutProvider); + } + + private static async Task AssertCleanupIsPerformed(SutProvider sutProvider) + { + await sutProvider.GetDependency() + .Received(1) + .CancelAndRecoverChargesAsync(Arg.Any()); + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(Arg.Any()); + await sutProvider.GetDependency() + .Received(1) + .DeleteOrganizationAbilityAsync(Arg.Any()); + } +} From 86a4ce5a51401705cb8a9fde7c74a5e19c7a3b8d Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Tue, 24 Jun 2025 09:13:43 -0500 Subject: [PATCH 22/47] [PM-20576] OrganizationReport - Queries and Command (#5983) --- .../AddOrganizationReportCommand.cs | 74 +++++++ .../DropOrganizationReportCommand.cs | 42 ++++ .../GetOrganizationReportQuery.cs | 43 ++++ .../IAddOrganizationReportCommand.cs | 10 + .../IDropOrganizationReportCommand.cs | 9 + .../Interfaces/IGetOrganizationReportQuery.cs | 9 + .../ReportingServiceCollectionExtensions.cs | 3 + .../Requests/AddOrganizationReportRequest.cs | 8 + .../Requests/DropOrganizationReportRequest.cs | 7 + .../IOrganizationReportRepository.cs | 2 + .../Dirt/OrganizationReportRepository.cs | 10 + .../OrganizationReportRepository.cs | 18 ++ .../AddOrganizationReportCommandTests.cs | 133 ++++++++++++ .../DeleteOrganizationReportCommandTests.cs | 194 ++++++++++++++++++ .../GetOrganizationReportQueryTests.cs | 188 +++++++++++++++++ .../EntityFrameworkRepositoryFixtures.cs | 1 + .../AutoFixture/OrganizationReportFixture.cs | 80 ++++++++ .../OrganizationReportRepositoryTests.cs | 131 ++++++++++++ .../Infrastructure.EFIntegration.Test.csproj | 1 - 19 files changed, 962 insertions(+), 1 deletion(-) create mode 100644 src/Core/Dirt/Reports/ReportFeatures/AddOrganizationReportCommand.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/DropOrganizationReportCommand.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportQuery.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddOrganizationReportCommand.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Interfaces/IDropOrganizationReportCommand.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportQuery.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Requests/AddOrganizationReportRequest.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Requests/DropOrganizationReportRequest.cs create mode 100644 test/Core.Test/Dirt/ReportFeatures/AddOrganizationReportCommandTests.cs create mode 100644 test/Core.Test/Dirt/ReportFeatures/DeleteOrganizationReportCommandTests.cs create mode 100644 test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportQueryTests.cs create mode 100644 test/Infrastructure.EFIntegration.Test/AutoFixture/OrganizationReportFixture.cs create mode 100644 test/Infrastructure.EFIntegration.Test/Dirt/Repositories/OrganizationReportRepositoryTests.cs diff --git a/src/Core/Dirt/Reports/ReportFeatures/AddOrganizationReportCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/AddOrganizationReportCommand.cs new file mode 100644 index 0000000000..66d25cdf56 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/AddOrganizationReportCommand.cs @@ -0,0 +1,74 @@ +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 AddOrganizationReportCommand : IAddOrganizationReportCommand +{ + private readonly IOrganizationRepository _organizationRepo; + private readonly IOrganizationReportRepository _organizationReportRepo; + private ILogger _logger; + + public AddOrganizationReportCommand( + IOrganizationRepository organizationRepository, + IOrganizationReportRepository organizationReportRepository, + ILogger logger) + { + _organizationRepo = organizationRepository; + _organizationReportRepo = organizationReportRepository; + _logger = logger; + } + + public async Task AddOrganizationReportAsync(AddOrganizationReportRequest request) + { + _logger.LogInformation("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); + throw new BadRequestException(errorMessage); + } + + var organizationReport = new OrganizationReport + { + OrganizationId = request.OrganizationId, + ReportData = request.ReportData, + Date = request.Date == default ? DateTime.UtcNow : request.Date, + CreationDate = DateTime.UtcNow, + }; + + organizationReport.SetNewId(); + + var data = await _organizationReportRepo.CreateAsync(organizationReport); + + _logger.LogInformation("Successfully added organization report for organization {organizationId}, {organizationReportId}", + request.OrganizationId, data.Id); + + return data; + } + + private async Task<(bool IsValid, string errorMessage)> ValidateRequestAsync( + AddOrganizationReportRequest request) + { + // verify that the organization exists + var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId); + if (organization == null) + { + return (false, "Invalid Organization"); + } + + // ensure that we have report data + if (string.IsNullOrWhiteSpace(request.ReportData)) + { + return (false, "Report 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 new file mode 100644 index 0000000000..e382788273 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/DropOrganizationReportCommand.cs @@ -0,0 +1,42 @@ +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 _ => + { + _logger.LogInformation("Dropping organization report {organizationReportId} for organization {organizationId}", + _.Id, request.OrganizationId); + + await _organizationReportRepo.DeleteAsync(_); + }); + } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportQuery.cs new file mode 100644 index 0000000000..e536fdfddc --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportQuery.cs @@ -0,0 +1,43 @@ +using Bit.Core.Dirt.Entities; +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 GetOrganizationReportQuery : IGetOrganizationReportQuery +{ + private IOrganizationReportRepository _organizationReportRepo; + private ILogger _logger; + + public GetOrganizationReportQuery( + IOrganizationReportRepository organizationReportRepo, + ILogger logger) + { + _organizationReportRepo = organizationReportRepo; + _logger = logger; + } + + public async Task> GetOrganizationReportAsync(Guid organizationId) + { + if (organizationId == Guid.Empty) + { + throw new BadRequestException("OrganizationId is required."); + } + + _logger.LogInformation("Fetching organization reports for organization {organizationId}", organizationId); + return await _organizationReportRepo.GetByOrganizationIdAsync(organizationId); + } + + public async Task GetLatestOrganizationReportAsync(Guid organizationId) + { + if (organizationId == Guid.Empty) + { + throw new BadRequestException("OrganizationId is required."); + } + + _logger.LogInformation("Fetching latest organization report for organization {organizationId}", organizationId); + return await _organizationReportRepo.GetLatestByOrganizationIdAsync(organizationId); + } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddOrganizationReportCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddOrganizationReportCommand.cs new file mode 100644 index 0000000000..3677b9794b --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddOrganizationReportCommand.cs @@ -0,0 +1,10 @@ + +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; + +public interface IAddOrganizationReportCommand +{ + Task AddOrganizationReportAsync(AddOrganizationReportRequest request); +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IDropOrganizationReportCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IDropOrganizationReportCommand.cs new file mode 100644 index 0000000000..1ed9059f56 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IDropOrganizationReportCommand.cs @@ -0,0 +1,9 @@ + +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/IGetOrganizationReportQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportQuery.cs new file mode 100644 index 0000000000..f596e8f517 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportQuery.cs @@ -0,0 +1,9 @@ +using Bit.Core.Dirt.Entities; + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; + +public interface IGetOrganizationReportQuery +{ + Task> GetOrganizationReportAsync(Guid organizationId); + Task GetLatestOrganizationReportAsync(Guid organizationId); +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs b/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs index 4339d0f2f4..a20c7a3e8f 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs @@ -13,5 +13,8 @@ public static class ReportingServiceCollectionExtensions 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 new file mode 100644 index 0000000000..ca892cddde --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/AddOrganizationReportRequest.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; + +public class AddOrganizationReportRequest +{ + public Guid OrganizationId { get; set; } + public string ReportData { get; set; } + public DateTime Date { get; set; } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Requests/DropOrganizationReportRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/DropOrganizationReportRequest.cs new file mode 100644 index 0000000000..cc889fe351 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/DropOrganizationReportRequest.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; + +public class DropOrganizationReportRequest +{ + public Guid OrganizationId { get; set; } + public IEnumerable OrganizationReportIds { get; set; } +} diff --git a/src/Core/Dirt/Repositories/IOrganizationReportRepository.cs b/src/Core/Dirt/Repositories/IOrganizationReportRepository.cs index 6efb6cf23a..e7979ca4b7 100644 --- a/src/Core/Dirt/Repositories/IOrganizationReportRepository.cs +++ b/src/Core/Dirt/Repositories/IOrganizationReportRepository.cs @@ -6,5 +6,7 @@ namespace Bit.Core.Dirt.Repositories; public interface IOrganizationReportRepository : IRepository { Task> GetByOrganizationIdAsync(Guid organizationId); + + Task GetLatestByOrganizationIdAsync(Guid organizationId); } diff --git a/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs b/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs index 88478c31ae..7a5fe1c8c2 100644 --- a/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs +++ b/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs @@ -32,4 +32,14 @@ public class OrganizationReportRepository : Repository return results.ToList(); } } + + public async Task GetLatestByOrganizationIdAsync(Guid organizationId) + { + return await GetByOrganizationIdAsync(organizationId) + .ContinueWith(task => + { + var reports = task.Result; + return reports.OrderByDescending(r => r.CreationDate).FirstOrDefault(); + }); + } } diff --git a/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs index 7c7bd56dae..416fd91933 100644 --- a/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs +++ b/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs @@ -27,4 +27,22 @@ public class OrganizationReportRepository : return Mapper.Map>(results); } } + + public async Task GetLatestByOrganizationIdAsync(Guid organizationId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var result = await dbContext.OrganizationReports + .Where(p => p.OrganizationId == organizationId) + .OrderByDescending(p => p.Date) + .Take(1) + .FirstOrDefaultAsync(); + + if (result == null) + return default; + + return Mapper.Map(result); + } + } } diff --git a/test/Core.Test/Dirt/ReportFeatures/AddOrganizationReportCommandTests.cs b/test/Core.Test/Dirt/ReportFeatures/AddOrganizationReportCommandTests.cs new file mode 100644 index 0000000000..618558142b --- /dev/null +++ b/test/Core.Test/Dirt/ReportFeatures/AddOrganizationReportCommandTests.cs @@ -0,0 +1,133 @@ + +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 AddOrganizationReportCommandTests +{ + + [Theory] + [BitAutoData] + public async Task AddOrganizationReportAsync_ShouldReturnOrganizationReport( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(fixture.Create()); + + sutProvider.GetDependency() + .CreateAsync(Arg.Any()) + .Returns(c => c.Arg()); + + // Act + var result = await sutProvider.Sut.AddOrganizationReportAsync(request); + + // Assert + Assert.NotNull(result); + } + + [Theory] + [BitAutoData] + public async Task AddOrganizationReportAsync_WithInvalidOrganizationId_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(null as Organization); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.AddOrganizationReportAsync(request)); + Assert.Equal("Invalid Organization", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task AddOrganizationReportAsync_WithInvalidUrl_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .Without(_ => _.ReportData) + .Create(); + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(fixture.Create()); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.AddOrganizationReportAsync(request)); + Assert.Equal("Report Data is required", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task AddOrganizationReportAsync_Multiples_WithInvalidOrganizationId_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(null as Organization); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.AddOrganizationReportAsync(request)); + Assert.Equal("Invalid Organization", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task AddOrganizationReportAsync_Multiples_WithInvalidUrl_ShouldThrowError( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .Without(_ => _.ReportData) + .Create(); + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(fixture.Create()); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.AddOrganizationReportAsync(request)); + Assert.Equal("Report Data is required", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task AddOrganizationReportAsync_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.AddOrganizationReportAsync(request)); + Assert.Equal("Invalid Organization", exception.Message); + } +} diff --git a/test/Core.Test/Dirt/ReportFeatures/DeleteOrganizationReportCommandTests.cs b/test/Core.Test/Dirt/ReportFeatures/DeleteOrganizationReportCommandTests.cs new file mode 100644 index 0000000000..f6a5c13be9 --- /dev/null +++ b/test/Core.Test/Dirt/ReportFeatures/DeleteOrganizationReportCommandTests.cs @@ -0,0 +1,194 @@ +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/GetOrganizationReportQueryTests.cs b/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportQueryTests.cs new file mode 100644 index 0000000000..19d020be12 --- /dev/null +++ b/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportQueryTests.cs @@ -0,0 +1,188 @@ +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/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs b/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs index 4a56d2cb22..5c7b3ed99d 100644 --- a/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs +++ b/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs @@ -92,6 +92,7 @@ public class EfRepositoryListBuilder : ISpecimenBuilder where T : BaseEntityF cfg.AddProfile(); cfg.AddProfile(); cfg.AddProfile(); + cfg.AddProfile(); }) .CreateMapper())); diff --git a/test/Infrastructure.EFIntegration.Test/AutoFixture/OrganizationReportFixture.cs b/test/Infrastructure.EFIntegration.Test/AutoFixture/OrganizationReportFixture.cs new file mode 100644 index 0000000000..b5b2626d7a --- /dev/null +++ b/test/Infrastructure.EFIntegration.Test/AutoFixture/OrganizationReportFixture.cs @@ -0,0 +1,80 @@ +using AutoFixture; +using AutoFixture.Kernel; +using Bit.Core.Dirt.Entities; +using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; +using Bit.Infrastructure.EntityFramework.Dirt.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Infrastructure.EFIntegration.Test.AutoFixture; + +internal class OrganizationReportBuilder : ISpecimenBuilder +{ + public object Create(object request, ISpecimenContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var type = request as Type; + if (type == null || type != typeof(OrganizationReport)) + { + return new NoSpecimen(); + } + + var fixture = new Fixture(); + var obj = fixture.WithAutoNSubstitutions().Create(); + return obj; + } +} + +internal class EfOrganizationReport : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customizations.Add(new IgnoreVirtualMembersCustomization()); + fixture.Customizations.Add(new GlobalSettingsBuilder()); + fixture.Customizations.Add(new OrganizationReportBuilder()); + fixture.Customizations.Add(new OrganizationBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + } +} + +internal class EfOrganizationReportApplicableToUser : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customizations.Add(new IgnoreVirtualMembersCustomization()); + fixture.Customizations.Add(new GlobalSettingsBuilder()); + fixture.Customizations.Add(new OrganizationReportBuilder()); + fixture.Customizations.Add(new OrganizationBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + } +} + +internal class EfOrganizationReportAutoDataAttribute : CustomAutoDataAttribute +{ + public EfOrganizationReportAutoDataAttribute() : base(new SutProviderCustomization(), new EfOrganizationReport()) { } +} + +internal class EfOrganizationReportApplicableToUserInlineAutoDataAttribute : InlineCustomAutoDataAttribute +{ + public EfOrganizationReportApplicableToUserInlineAutoDataAttribute(params object[] values) + : base(new[] { typeof(SutProviderCustomization), typeof(EfOrganizationReportApplicableToUser) }, values) { } +} + +internal class InlineEfOrganizationReportAutoDataAttribute : InlineCustomAutoDataAttribute +{ + public InlineEfOrganizationReportAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization), + typeof(EfPolicy) }, values) + { } +} diff --git a/test/Infrastructure.EFIntegration.Test/Dirt/Repositories/OrganizationReportRepositoryTests.cs b/test/Infrastructure.EFIntegration.Test/Dirt/Repositories/OrganizationReportRepositoryTests.cs new file mode 100644 index 0000000000..dd2adc0970 --- /dev/null +++ b/test/Infrastructure.EFIntegration.Test/Dirt/Repositories/OrganizationReportRepositoryTests.cs @@ -0,0 +1,131 @@ +using AutoFixture; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Repositories; +using Bit.Core.Test.AutoFixture.Attributes; +using Bit.Infrastructure.Dapper.Dirt; +using Bit.Infrastructure.EFIntegration.Test.AutoFixture; +using Xunit; +using EfRepo = Bit.Infrastructure.EntityFramework.Repositories; +using SqlRepo = Bit.Infrastructure.Dapper.Repositories; + +namespace Bit.Infrastructure.EFIntegration.Test.Dirt.Repositories; + +public class OrganizationReportRepositoryTests +{ + [CiSkippedTheory, EfOrganizationReportAutoData] + public async Task CreateAsync_ShouldCreateReport_WhenValidDataProvided( + OrganizationReport report, + Organization organization, + List suts, + List efOrganizationRepos, + OrganizationReportRepository sqlOrganizationReportRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + var records = new List(); + foreach (var sut in suts) + { + var i = suts.IndexOf(sut); + + var efOrganization = await efOrganizationRepos[i].CreateAsync(organization); + sut.ClearChangeTracking(); + + report.OrganizationId = efOrganization.Id; + var postEfOrganizationReport = await sut.CreateAsync(report); + sut.ClearChangeTracking(); + + var savedOrganizationReport = await sut.GetByIdAsync(postEfOrganizationReport.Id); + records.Add(savedOrganizationReport); + } + + var sqlOrganization = await sqlOrganizationRepo.CreateAsync(organization); + + report.OrganizationId = sqlOrganization.Id; + var sqlOrgnizationReportRecord = await sqlOrganizationReportRepo.CreateAsync(report); + var savedSqlOrganizationReport = await sqlOrganizationReportRepo.GetByIdAsync(sqlOrgnizationReportRecord.Id); + records.Add(savedSqlOrganizationReport); + + Assert.True(records.Count == 4); + } + + [CiSkippedTheory, EfOrganizationReportAutoData] + public async Task RetrieveByOrganisation_Works( + OrganizationReportRepository sqlPasswordHealthReportApplicationRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + var (firstOrg, _) = await CreateOrganizationAndReportAsync(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo); + var (secondOrg, _) = await CreateOrganizationAndReportAsync(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo); + + var firstSetOfRecords = await sqlPasswordHealthReportApplicationRepo.GetByOrganizationIdAsync(firstOrg.Id); + var nextSetOfRecords = await sqlPasswordHealthReportApplicationRepo.GetByOrganizationIdAsync(secondOrg.Id); + + Assert.True(firstSetOfRecords.Count == 1 && firstSetOfRecords.First().OrganizationId == firstOrg.Id); + Assert.True(nextSetOfRecords.Count == 1 && nextSetOfRecords.First().OrganizationId == secondOrg.Id); + } + + [CiSkippedTheory, EfOrganizationReportAutoData] + public async Task Delete_Works( + List suts, + List efOrganizationRepos, + OrganizationReportRepository sqlOrganizationReportRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + var fixture = new Fixture(); + var rawOrg = fixture.Build().Create(); + var rawRecord = fixture.Build() + .With(_ => _.OrganizationId, rawOrg.Id) + .Create(); + var dbRecords = new List(); + + foreach (var sut in suts) + { + var i = suts.IndexOf(sut); + + // create a new organization for each repository + var organization = await efOrganizationRepos[i].CreateAsync(rawOrg); + + // map the organization Id and use Upsert to save new record + rawRecord.OrganizationId = organization.Id; + rawRecord = await sut.CreateAsync(rawRecord); + sut.ClearChangeTracking(); + + // apply update using Upsert to make changes to db + await sut.DeleteAsync(rawRecord); + sut.ClearChangeTracking(); + + // retrieve the data and add to the list for assertions + var recordFromDb = await sut.GetByIdAsync(rawRecord.Id); + dbRecords.Add(recordFromDb); + + sut.ClearChangeTracking(); + } + + // sql - create new records + var (org, organizationReport) = await CreateOrganizationAndReportAsync(sqlOrganizationRepo, sqlOrganizationReportRepo); + await sqlOrganizationReportRepo.DeleteAsync(organizationReport); + var sqlDbRecord = await sqlOrganizationReportRepo.GetByIdAsync(organizationReport.Id); + dbRecords.Add(sqlDbRecord); + + // assertions + // all records should be null - as they were deleted before querying + Assert.True(dbRecords.Where(_ => _ == null).Count() == 4); + } + + private async Task<(Organization, OrganizationReport)> CreateOrganizationAndReportAsync( + IOrganizationRepository orgRepo, + IOrganizationReportRepository orgReportRepo) + { + var fixture = new Fixture(); + var organization = fixture.Create(); + + var orgReportRecord = fixture.Build() + .With(x => x.OrganizationId, organization.Id) + .Create(); + + organization = await orgRepo.CreateAsync(organization); + orgReportRecord = await orgReportRepo.CreateAsync(orgReportRecord); + + return (organization, orgReportRecord); + } +} diff --git a/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj b/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj index 8c2eb50c5b..e63d3d7419 100644 --- a/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj +++ b/test/Infrastructure.EFIntegration.Test/Infrastructure.EFIntegration.Test.csproj @@ -30,7 +30,6 @@ - From 70703cb3b09fde1b8385402e615646acf358da65 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Tue, 24 Jun 2025 10:29:09 -0400 Subject: [PATCH 23/47] [PM-20633] rename personal ownership (#5978) * rename personal ownership * rename enums, files, tests --- src/Core/AdminConsole/Enums/PolicyType.cs | 4 +- .../ConfirmOrganizationUserCommand.cs | 6 +- ...anizationDataOwnershipPolicyRequirement.cs | 72 +++++++++++++++++++ .../PersonalOwnershipPolicyRequirement.cs | 72 ------------------- .../PolicyServiceCollectionExtensions.cs | 2 +- .../ImportFeatures/ImportCiphersCommand.cs | 8 +-- .../Services/Implementations/CipherService.cs | 8 +-- .../ConfirmOrganizationUserCommandTests.cs | 24 +++---- ...OwnershipPolicyRequirementFactoryTests.cs} | 22 +++--- .../ImportCiphersAsyncCommandTests.cs | 20 +++--- .../Vault/Services/CipherServiceTests.cs | 24 +++---- 11 files changed, 131 insertions(+), 131 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs delete mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs rename test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/{PersonalOwnershipPolicyRequirementFactoryTests.cs => OrganizationDataOwnershipPolicyRequirementFactoryTests.cs} (58%) diff --git a/src/Core/AdminConsole/Enums/PolicyType.cs b/src/Core/AdminConsole/Enums/PolicyType.cs index f72637f862..ab39e543f8 100644 --- a/src/Core/AdminConsole/Enums/PolicyType.cs +++ b/src/Core/AdminConsole/Enums/PolicyType.cs @@ -7,7 +7,7 @@ public enum PolicyType : byte PasswordGenerator = 2, SingleOrg = 3, RequireSso = 4, - PersonalOwnership = 5, + OrganizationDataOwnership = 5, DisableSend = 6, SendOptions = 7, ResetPassword = 8, @@ -35,7 +35,7 @@ public static class PolicyTypeExtensions PolicyType.PasswordGenerator => "Password generator", PolicyType.SingleOrg => "Single organization", PolicyType.RequireSso => "Require single sign-on authentication", - PolicyType.PersonalOwnership => "Remove individual vault", + PolicyType.OrganizationDataOwnership => "Enforce organization data ownership", PolicyType.DisableSend => "Remove Send", PolicyType.SendOptions => "Send options", PolicyType.ResetPassword => "Account recovery administration", diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs index dd118d7ea3..62e5d60191 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs @@ -223,7 +223,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand private async Task HandleConfirmationSideEffectsAsync(Guid organizationId, OrganizationUser organizationUser, string defaultUserCollectionName) { - // Create DefaultUserCollection type collection for the user if the PersonalOwnership policy is enabled for the organization + // 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) { @@ -244,8 +244,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand return false; } - var personalOwnershipRequirement = await _policyRequirementQuery.GetAsync(userId); - return personalOwnershipRequirement.RequiresDefaultCollection(organizationId); + var organizationDataOwnershipRequirement = await _policyRequirementQuery.GetAsync(userId); + return organizationDataOwnershipRequirement.RequiresDefaultCollection(organizationId); } private async Task CreateDefaultCollectionAsync(Guid organizationId, Guid organizationUserId, string defaultCollectionName) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs new file mode 100644 index 0000000000..7ccb3f7807 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs @@ -0,0 +1,72 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +/// +/// Represents the Organization Data Ownership policy state. +/// +public enum OrganizationDataOwnershipState +{ + /// + /// Organization Data Ownership is enforced- members are required to save items to an organization. + /// + Enabled = 1, + + /// + /// Organization Data Ownership is not enforced- users can save items to their personal vault. + /// + Disabled = 2 +} + +/// +/// Policy requirements for the Organization data ownership policy +/// +public class OrganizationDataOwnershipPolicyRequirement : IPolicyRequirement +{ + private readonly IEnumerable _organizationIdsWithPolicyEnabled; + + /// + /// The organization data ownership state for the user. + /// + /// + /// The collection of Organization IDs that have the Organization Data Ownership policy enabled. + /// + public OrganizationDataOwnershipPolicyRequirement( + OrganizationDataOwnershipState organizationDataOwnershipState, + IEnumerable organizationIdsWithPolicyEnabled) + { + _organizationIdsWithPolicyEnabled = organizationIdsWithPolicyEnabled ?? []; + State = organizationDataOwnershipState; + } + + /// + /// The Organization data ownership policy state for the user. + /// + public OrganizationDataOwnershipState State { get; } + + /// + /// Returns true if the Organization Data Ownership policy is enforced in that organization. + /// + public bool RequiresDefaultCollection(Guid organizationId) + { + return _organizationIdsWithPolicyEnabled.Contains(organizationId); + } +} + +public class OrganizationDataOwnershipPolicyRequirementFactory : BasePolicyRequirementFactory +{ + public override PolicyType PolicyType => PolicyType.OrganizationDataOwnership; + + public override OrganizationDataOwnershipPolicyRequirement Create(IEnumerable policyDetails) + { + var organizationDataOwnershipState = policyDetails.Any() + ? OrganizationDataOwnershipState.Enabled + : OrganizationDataOwnershipState.Disabled; + var organizationIdsWithPolicyEnabled = policyDetails.Select(p => p.OrganizationId).ToHashSet(); + + return new OrganizationDataOwnershipPolicyRequirement( + organizationDataOwnershipState, + organizationIdsWithPolicyEnabled); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs deleted file mode 100644 index 219d3f1bf8..0000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; - -namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; - -/// -/// Represents the personal ownership policy state. -/// -public enum PersonalOwnershipState -{ - /// - /// Personal ownership is allowed - users can save items to their personal vault. - /// - Allowed, - - /// - /// Personal ownership is restricted - members are required to save items to an organization. - /// - Restricted -} - -/// -/// Policy requirements for the Disable Personal Ownership policy. -/// -public class PersonalOwnershipPolicyRequirement : IPolicyRequirement -{ - private readonly IEnumerable _organizationIdsWithPolicyEnabled; - - /// - /// The personal ownership state for the user. - /// - /// - /// The collection of Organization IDs that have the Disable Personal Ownership policy enabled. - /// - public PersonalOwnershipPolicyRequirement( - PersonalOwnershipState personalOwnershipState, - IEnumerable organizationIdsWithPolicyEnabled) - { - _organizationIdsWithPolicyEnabled = organizationIdsWithPolicyEnabled ?? []; - State = personalOwnershipState; - } - - /// - /// The personal ownership policy state for the user. - /// - public PersonalOwnershipState State { get; } - - /// - /// Returns true if the Disable Personal Ownership policy is enforced in that organization. - /// - public bool RequiresDefaultCollection(Guid organizationId) - { - return _organizationIdsWithPolicyEnabled.Contains(organizationId); - } -} - -public class PersonalOwnershipPolicyRequirementFactory : BasePolicyRequirementFactory -{ - public override PolicyType PolicyType => PolicyType.PersonalOwnership; - - public override PersonalOwnershipPolicyRequirement Create(IEnumerable policyDetails) - { - var personalOwnershipState = policyDetails.Any() - ? PersonalOwnershipState.Restricted - : PersonalOwnershipState.Allowed; - var organizationIdsWithPolicyEnabled = policyDetails.Select(p => p.OrganizationId).ToHashSet(); - - return new PersonalOwnershipPolicyRequirement( - personalOwnershipState, - organizationIdsWithPolicyEnabled); - } -} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index f98135b70d..87fdcbe543 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -34,7 +34,7 @@ public static class PolicyServiceCollectionExtensions services.AddScoped, DisableSendPolicyRequirementFactory>(); services.AddScoped, SendOptionsPolicyRequirementFactory>(); services.AddScoped, ResetPasswordPolicyRequirementFactory>(); - services.AddScoped, PersonalOwnershipPolicyRequirementFactory>(); + services.AddScoped, OrganizationDataOwnershipPolicyRequirementFactory>(); services.AddScoped, RequireSsoPolicyRequirementFactory>(); services.AddScoped, RequireTwoFactorPolicyRequirementFactory>(); } diff --git a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs index 9fc6238143..829eedc34d 100644 --- a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs +++ b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs @@ -55,11 +55,11 @@ public class ImportCiphersCommand : IImportCiphersCommand Guid importingUserId) { // Make sure the user can save new ciphers to their personal vault - var isPersonalVaultRestricted = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements) - ? (await _policyRequirementQuery.GetAsync(importingUserId)).State == PersonalOwnershipState.Restricted - : await _policyService.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.PersonalOwnership); + var organizationDataOwnershipEnabled = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements) + ? (await _policyRequirementQuery.GetAsync(importingUserId)).State == OrganizationDataOwnershipState.Enabled + : await _policyService.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.OrganizationDataOwnership); - if (isPersonalVaultRestricted) + if (organizationDataOwnershipEnabled) { throw new BadRequestException("You cannot import items into your personal vault because you are " + "a member of an organization which forbids it."); diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index 5fa27039c2..42221adf4b 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -142,11 +142,11 @@ public class CipherService : ICipherService } else { - var isPersonalVaultRestricted = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements) - ? (await _policyRequirementQuery.GetAsync(savingUserId)).State == PersonalOwnershipState.Restricted - : await _policyService.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership); + var organizationDataOwnershipEnabled = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements) + ? (await _policyRequirementQuery.GetAsync(savingUserId)).State == OrganizationDataOwnershipState.Enabled + : await _policyService.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.OrganizationDataOwnership); - if (isPersonalVaultRestricted) + if (organizationDataOwnershipEnabled) { throw new BadRequestException("Due to an Enterprise Policy, you are restricted from saving items to your personal vault."); } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs index 1732f61d5e..0bb38f7d0b 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs @@ -445,7 +445,7 @@ public class ConfirmOrganizationUserCommandTests } [Theory, BitAutoData] - public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithPersonalOwnershipPolicyApplicable_WithValidCollectionName_CreatesDefaultCollection( + public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithOrganizationDataOwnershipPolicyApplicable_WithValidCollectionName_CreatesDefaultCollection( Organization organization, OrganizationUser confirmingUser, [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, string key, string collectionName, SutProvider sutProvider) @@ -461,9 +461,9 @@ public class ConfirmOrganizationUserCommandTests sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true); sutProvider.GetDependency() - .GetAsync(user.Id) - .Returns(new PersonalOwnershipPolicyRequirement( - PersonalOwnershipState.Restricted, + .GetAsync(user.Id) + .Returns(new OrganizationDataOwnershipPolicyRequirement( + OrganizationDataOwnershipState.Enabled, [organization.Id])); await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName); @@ -482,7 +482,7 @@ public class ConfirmOrganizationUserCommandTests } [Theory, BitAutoData] - public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithPersonalOwnershipPolicyApplicable_WithInvalidCollectionName_DoesNotCreateDefaultCollection( + public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithOrganizationDataOwnershipPolicyApplicable_WithInvalidCollectionName_DoesNotCreateDefaultCollection( Organization org, OrganizationUser confirmingUser, [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, string key, SutProvider sutProvider) @@ -498,9 +498,9 @@ public class ConfirmOrganizationUserCommandTests sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true); sutProvider.GetDependency() - .GetAsync(user.Id) - .Returns(new PersonalOwnershipPolicyRequirement( - PersonalOwnershipState.Restricted, + .GetAsync(user.Id) + .Returns(new OrganizationDataOwnershipPolicyRequirement( + OrganizationDataOwnershipState.Enabled, [org.Id])); await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, ""); @@ -511,7 +511,7 @@ public class ConfirmOrganizationUserCommandTests } [Theory, BitAutoData] - public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithPersonalOwnershipPolicyNotApplicable_DoesNotCreateDefaultCollection( + public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithOrganizationDataOwnershipPolicyNotApplicable_DoesNotCreateDefaultCollection( Organization org, OrganizationUser confirmingUser, [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, string key, string collectionName, SutProvider sutProvider) @@ -526,9 +526,9 @@ public class ConfirmOrganizationUserCommandTests sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true); sutProvider.GetDependency() - .GetAsync(user.Id) - .Returns(new PersonalOwnershipPolicyRequirement( - PersonalOwnershipState.Restricted, + .GetAsync(user.Id) + .Returns(new OrganizationDataOwnershipPolicyRequirement( + OrganizationDataOwnershipState.Enabled, [Guid.NewGuid()])); await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirementFactoryTests.cs similarity index 58% rename from test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirementFactoryTests.cs rename to test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirementFactoryTests.cs index dfac394243..95037efb97 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirementFactoryTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirementFactoryTests.cs @@ -9,30 +9,30 @@ using Xunit; namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; [SutProviderCustomize] -public class PersonalOwnershipPolicyRequirementFactoryTests +public class OrganizationDataOwnershipPolicyRequirementFactoryTests { [Theory, BitAutoData] - public void State_WithNoPolicies_ReturnsAllowed(SutProvider sutProvider) + public void State_WithNoPolicies_ReturnsAllowed(SutProvider sutProvider) { var actual = sutProvider.Sut.Create([]); - Assert.Equal(PersonalOwnershipState.Allowed, actual.State); + Assert.Equal(OrganizationDataOwnershipState.Disabled, actual.State); } [Theory, BitAutoData] - public void State_WithPersonalOwnershipPolicies_ReturnsRestricted( - [PolicyDetails(PolicyType.PersonalOwnership)] PolicyDetails[] policies, - SutProvider sutProvider) + public void State_WithOrganizationDataOwnershipPolicies_ReturnsRestricted( + [PolicyDetails(PolicyType.OrganizationDataOwnership)] PolicyDetails[] policies, + SutProvider sutProvider) { var actual = sutProvider.Sut.Create(policies); - Assert.Equal(PersonalOwnershipState.Restricted, actual.State); + Assert.Equal(OrganizationDataOwnershipState.Enabled, actual.State); } [Theory, BitAutoData] public void RequiresDefaultCollection_WithNoPolicies_ReturnsFalse( Guid organizationId, - SutProvider sutProvider) + SutProvider sutProvider) { var actual = sutProvider.Sut.Create([]); @@ -40,10 +40,10 @@ public class PersonalOwnershipPolicyRequirementFactoryTests } [Theory, BitAutoData] - public void RequiresDefaultCollection_WithPersonalOwnershipPolicies_ReturnsCorrectResult( - [PolicyDetails(PolicyType.PersonalOwnership)] PolicyDetails[] policies, + public void RequiresDefaultCollection_WithOrganizationDataOwnershipPolicies_ReturnsCorrectResult( + [PolicyDetails(PolicyType.OrganizationDataOwnership)] PolicyDetails[] policies, Guid nonPolicyOrganizationId, - SutProvider sutProvider) + SutProvider sutProvider) { var actual = sutProvider.Sut.Create(policies); diff --git a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs index f61c2f4443..1b50779c57 100644 --- a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs +++ b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs @@ -31,7 +31,7 @@ public class ImportCiphersAsyncCommandTests SutProvider sutProvider) { sutProvider.GetDependency() - .AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.PersonalOwnership) + .AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.OrganizationDataOwnership) .Returns(false); sutProvider.GetDependency() @@ -51,7 +51,7 @@ public class ImportCiphersAsyncCommandTests } [Theory, BitAutoData] - public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyDisabled_Success( + public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithOrganizationDataOwnershipPolicyDisabled_Success( Guid importingUserId, List ciphers, SutProvider sutProvider) @@ -61,9 +61,9 @@ public class ImportCiphersAsyncCommandTests .Returns(true); sutProvider.GetDependency() - .GetAsync(importingUserId) - .Returns(new PersonalOwnershipPolicyRequirement( - PersonalOwnershipState.Allowed, + .GetAsync(importingUserId) + .Returns(new OrganizationDataOwnershipPolicyRequirement( + OrganizationDataOwnershipState.Disabled, [])); sutProvider.GetDependency() @@ -91,7 +91,7 @@ public class ImportCiphersAsyncCommandTests ciphers.ForEach(c => c.UserId = userId); sutProvider.GetDependency() - .AnyPoliciesApplicableToUserAsync(userId, PolicyType.PersonalOwnership) + .AnyPoliciesApplicableToUserAsync(userId, PolicyType.OrganizationDataOwnership) .Returns(true); var folderRelationships = new List>(); @@ -103,7 +103,7 @@ public class ImportCiphersAsyncCommandTests } [Theory, BitAutoData] - public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyEnabled_ThrowsBadRequestException( + public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithOrganizationDataOwnershipPolicyEnabled_ThrowsBadRequestException( List folders, List ciphers, SutProvider sutProvider) @@ -117,9 +117,9 @@ public class ImportCiphersAsyncCommandTests .Returns(true); sutProvider.GetDependency() - .GetAsync(userId) - .Returns(new PersonalOwnershipPolicyRequirement( - PersonalOwnershipState.Restricted, + .GetAsync(userId) + .Returns(new OrganizationDataOwnershipPolicyRequirement( + OrganizationDataOwnershipState.Enabled, [Guid.NewGuid()])); var folderRelationships = new List>(); diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs index 1bd41e9f01..0cee6530c2 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -114,7 +114,7 @@ public class CipherServiceTests [Theory] [BitAutoData] - public async Task SaveDetailsAsync_PersonalVault_WithDisablePersonalOwnershipPolicyEnabled_Throws( + public async Task SaveDetailsAsync_PersonalVault_WithOrganizationDataOwnershipPolicyEnabled_Throws( SutProvider sutProvider, CipherDetails cipher, Guid savingUserId) @@ -124,7 +124,7 @@ public class CipherServiceTests cipher.OrganizationId = null; sutProvider.GetDependency() - .AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership) + .AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.OrganizationDataOwnership) .Returns(true); var exception = await Assert.ThrowsAsync( @@ -134,7 +134,7 @@ public class CipherServiceTests [Theory] [BitAutoData] - public async Task SaveDetailsAsync_PersonalVault_WithDisablePersonalOwnershipPolicyDisabled_Succeeds( + public async Task SaveDetailsAsync_PersonalVault_WithOrganizationDataOwnershipPolicyDisabled_Succeeds( SutProvider sutProvider, CipherDetails cipher, Guid savingUserId) @@ -144,7 +144,7 @@ public class CipherServiceTests cipher.OrganizationId = null; sutProvider.GetDependency() - .AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership) + .AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.OrganizationDataOwnership) .Returns(false); await sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null); @@ -156,7 +156,7 @@ public class CipherServiceTests [Theory] [BitAutoData] - public async Task SaveDetailsAsync_PersonalVault_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyEnabled_Throws( + public async Task SaveDetailsAsync_PersonalVault_WithPolicyRequirementsEnabled_WithOrganizationDataOwnershipPolicyEnabled_Throws( SutProvider sutProvider, CipherDetails cipher, Guid savingUserId) @@ -170,9 +170,9 @@ public class CipherServiceTests .Returns(true); sutProvider.GetDependency() - .GetAsync(savingUserId) - .Returns(new PersonalOwnershipPolicyRequirement( - PersonalOwnershipState.Restricted, + .GetAsync(savingUserId) + .Returns(new OrganizationDataOwnershipPolicyRequirement( + OrganizationDataOwnershipState.Enabled, [Guid.NewGuid()])); var exception = await Assert.ThrowsAsync( @@ -182,7 +182,7 @@ public class CipherServiceTests [Theory] [BitAutoData] - public async Task SaveDetailsAsync_PersonalVault_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyDisabled_Succeeds( + public async Task SaveDetailsAsync_PersonalVault_WithPolicyRequirementsEnabled_WithOrganizationDataOwnershipPolicyDisabled_Succeeds( SutProvider sutProvider, CipherDetails cipher, Guid savingUserId) @@ -196,9 +196,9 @@ public class CipherServiceTests .Returns(true); sutProvider.GetDependency() - .GetAsync(savingUserId) - .Returns(new PersonalOwnershipPolicyRequirement( - PersonalOwnershipState.Allowed, + .GetAsync(savingUserId) + .Returns(new OrganizationDataOwnershipPolicyRequirement( + OrganizationDataOwnershipState.Disabled, [])); await sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null); From 34580f0472b3a17f5587a46c408e8d19c33b4484 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 24 Jun 2025 16:32:38 +0200 Subject: [PATCH 24/47] Remove key rotation v1 (#5939) --- .../Auth/Controllers/AccountsController.cs | 85 +----------- .../UserServiceCollectionExtensions.cs | 1 - .../Models/Data/RotateUserKeyData.cs | 20 --- .../UserKey/IRotateUserAccountKeysCommand.cs | 10 ++ .../UserKey/IRotateUserKeyCommand.cs | 29 ----- .../Implementations/RotateUserKeyCommand.cs | 121 ------------------ .../Controllers/AccountsControllerTests.cs | 45 +------ .../UserKey/RotateUserKeyCommandTests.cs | 86 ------------- 8 files changed, 13 insertions(+), 384 deletions(-) delete mode 100644 src/Core/KeyManagement/Models/Data/RotateUserKeyData.cs delete mode 100644 src/Core/KeyManagement/UserKey/IRotateUserKeyCommand.cs delete mode 100644 src/Core/KeyManagement/UserKey/Implementations/RotateUserKeyCommand.cs delete mode 100644 test/Core.Test/KeyManagement/UserKey/RotateUserKeyCommandTests.cs diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 2499b269f5..ec542daec7 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -1,34 +1,21 @@ -using Bit.Api.AdminConsole.Models.Request.Organizations; -using Bit.Api.AdminConsole.Models.Response; -using Bit.Api.Auth.Models.Request; +using Bit.Api.AdminConsole.Models.Response; using Bit.Api.Auth.Models.Request.Accounts; -using Bit.Api.Auth.Models.Request.WebAuthn; -using Bit.Api.KeyManagement.Validators; using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Response; -using Bit.Api.Tools.Models.Request; -using Bit.Api.Vault.Models.Request; using Bit.Core; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; -using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Api.Request.Accounts; -using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; -using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.KeyManagement.Models.Data; -using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Api.Response; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Entities; using Bit.Core.Utilities; -using Bit.Core.Vault.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -45,22 +32,9 @@ public class AccountsController : Controller private readonly IPolicyService _policyService; private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand; private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; - private readonly IRotateUserKeyCommand _rotateUserKeyCommand; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IFeatureService _featureService; - private readonly IRotationValidator, IEnumerable> _cipherValidator; - private readonly IRotationValidator, IEnumerable> _folderValidator; - private readonly IRotationValidator, IReadOnlyList> _sendValidator; - private readonly IRotationValidator, IEnumerable> - _emergencyAccessValidator; - private readonly IRotationValidator, - IReadOnlyList> - _organizationUserValidator; - private readonly IRotationValidator, IEnumerable> - _webauthnKeyValidator; - - public AccountsController( IOrganizationService organizationService, IOrganizationUserRepository organizationUserRepository, @@ -69,17 +43,8 @@ public class AccountsController : Controller IPolicyService policyService, ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand, ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand, - IRotateUserKeyCommand rotateUserKeyCommand, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IFeatureService featureService, - IRotationValidator, IEnumerable> cipherValidator, - IRotationValidator, IEnumerable> folderValidator, - IRotationValidator, IReadOnlyList> sendValidator, - IRotationValidator, IEnumerable> - emergencyAccessValidator, - IRotationValidator, IReadOnlyList> - organizationUserValidator, - IRotationValidator, IEnumerable> webAuthnKeyValidator + IFeatureService featureService ) { _organizationService = organizationService; @@ -89,15 +54,8 @@ public class AccountsController : Controller _policyService = policyService; _setInitialMasterPasswordCommand = setInitialMasterPasswordCommand; _tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand; - _rotateUserKeyCommand = rotateUserKeyCommand; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _featureService = featureService; - _cipherValidator = cipherValidator; - _folderValidator = folderValidator; - _sendValidator = sendValidator; - _emergencyAccessValidator = emergencyAccessValidator; - _organizationUserValidator = organizationUserValidator; - _webauthnKeyValidator = webAuthnKeyValidator; } @@ -313,45 +271,6 @@ public class AccountsController : Controller throw new BadRequestException(ModelState); } - [Obsolete("Replaced by the safer rotate-user-account-keys endpoint.")] - [HttpPost("key")] - public async Task PostKey([FromBody] UpdateKeyRequestModel model) - { - var user = await _userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - var dataModel = new RotateUserKeyData - { - MasterPasswordHash = model.MasterPasswordHash, - Key = model.Key, - PrivateKey = model.PrivateKey, - Ciphers = await _cipherValidator.ValidateAsync(user, model.Ciphers), - Folders = await _folderValidator.ValidateAsync(user, model.Folders), - Sends = await _sendValidator.ValidateAsync(user, model.Sends), - EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.EmergencyAccessKeys), - OrganizationUsers = await _organizationUserValidator.ValidateAsync(user, model.ResetPasswordKeys), - WebAuthnKeys = await _webauthnKeyValidator.ValidateAsync(user, model.WebAuthnKeys) - }; - - var result = await _rotateUserKeyCommand.RotateUserKeyAsync(user, dataModel); - - if (result.Succeeded) - { - return; - } - - foreach (var error in result.Errors) - { - ModelState.AddModelError(string.Empty, error.Description); - } - - await Task.Delay(2000); - throw new BadRequestException(ModelState); - } - [HttpPost("security-stamp")] public async Task PostSecurityStamp([FromBody] SecretVerificationRequestModel model) { diff --git a/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs b/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs index 7731e04af2..53bd8bdba2 100644 --- a/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs +++ b/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs @@ -38,7 +38,6 @@ public static class UserServiceCollectionExtensions public static void AddUserKeyCommands(this IServiceCollection services, IGlobalSettings globalSettings) { - services.AddScoped(); services.AddScoped(); } diff --git a/src/Core/KeyManagement/Models/Data/RotateUserKeyData.cs b/src/Core/KeyManagement/Models/Data/RotateUserKeyData.cs deleted file mode 100644 index 9813f760f3..0000000000 --- a/src/Core/KeyManagement/Models/Data/RotateUserKeyData.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Bit.Core.Auth.Entities; -using Bit.Core.Auth.Models.Data; -using Bit.Core.Entities; -using Bit.Core.Tools.Entities; -using Bit.Core.Vault.Entities; - -namespace Bit.Core.KeyManagement.Models.Data; - -public class RotateUserKeyData -{ - public string MasterPasswordHash { get; set; } - public string Key { get; set; } - public string PrivateKey { get; set; } - public IEnumerable Ciphers { get; set; } - public IEnumerable Folders { get; set; } - public IReadOnlyList Sends { get; set; } - public IEnumerable EmergencyAccesses { get; set; } - public IReadOnlyList OrganizationUsers { get; set; } - public IEnumerable WebAuthnKeys { get; set; } -} diff --git a/src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs b/src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs index ec40e7031d..1550f3c186 100644 --- a/src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs +++ b/src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs @@ -1,6 +1,7 @@ using Bit.Core.Entities; using Bit.Core.KeyManagement.Models.Data; using Microsoft.AspNetCore.Identity; +using Microsoft.Data.SqlClient; namespace Bit.Core.KeyManagement.UserKey; @@ -18,3 +19,12 @@ public interface IRotateUserAccountKeysCommand /// User KDF settings and email must match the model provided settings. Task RotateUserAccountKeysAsync(User user, RotateUserAccountKeysData model); } + +/// +/// A type used to implement updates to the database for key rotations. Each domain that requires an update of encrypted +/// data during a key rotation should use this to implement its own database call. The user repository loops through +/// these during a key rotation. +/// Note: connection and transaction are only used for Dapper. They won't be available in EF +/// +public delegate Task UpdateEncryptedDataForKeyRotation(SqlConnection connection = null, + SqlTransaction transaction = null); diff --git a/src/Core/KeyManagement/UserKey/IRotateUserKeyCommand.cs b/src/Core/KeyManagement/UserKey/IRotateUserKeyCommand.cs deleted file mode 100644 index 90dc90541f..0000000000 --- a/src/Core/KeyManagement/UserKey/IRotateUserKeyCommand.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Bit.Core.Entities; -using Bit.Core.KeyManagement.Models.Data; -using Microsoft.AspNetCore.Identity; -using Microsoft.Data.SqlClient; - -namespace Bit.Core.KeyManagement.UserKey; - -/// -/// Responsible for rotation of a user key and updating database with re-encrypted data -/// -public interface IRotateUserKeyCommand -{ - /// - /// Sets a new user key and updates all encrypted data. - /// - /// All necessary information for rotation. Warning: Any encrypted data not included will be lost. - /// An IdentityResult for verification of the master password hash - /// User must be provided. - Task RotateUserKeyAsync(User user, RotateUserKeyData model); -} - -/// -/// A type used to implement updates to the database for key rotations. Each domain that requires an update of encrypted -/// data during a key rotation should use this to implement its own database call. The user repository loops through -/// these during a key rotation. -/// Note: connection and transaction are only used for Dapper. They won't be available in EF -/// -public delegate Task UpdateEncryptedDataForKeyRotation(SqlConnection connection = null, - SqlTransaction transaction = null); diff --git a/src/Core/KeyManagement/UserKey/Implementations/RotateUserKeyCommand.cs b/src/Core/KeyManagement/UserKey/Implementations/RotateUserKeyCommand.cs deleted file mode 100644 index 8cece5f762..0000000000 --- a/src/Core/KeyManagement/UserKey/Implementations/RotateUserKeyCommand.cs +++ /dev/null @@ -1,121 +0,0 @@ -using Bit.Core.Auth.Repositories; -using Bit.Core.Entities; -using Bit.Core.KeyManagement.Models.Data; -using Bit.Core.Platform.Push; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Core.Tools.Repositories; -using Bit.Core.Vault.Repositories; -using Microsoft.AspNetCore.Identity; - -namespace Bit.Core.KeyManagement.UserKey.Implementations; - -/// -public class RotateUserKeyCommand : IRotateUserKeyCommand -{ - private readonly IUserService _userService; - private readonly IUserRepository _userRepository; - private readonly ICipherRepository _cipherRepository; - private readonly IFolderRepository _folderRepository; - private readonly ISendRepository _sendRepository; - private readonly IEmergencyAccessRepository _emergencyAccessRepository; - private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IPushNotificationService _pushService; - private readonly IdentityErrorDescriber _identityErrorDescriber; - private readonly IWebAuthnCredentialRepository _credentialRepository; - - /// - /// Instantiates a new - /// - /// Master password hash validation - /// Updates user keys and re-encrypted data if needed - /// Provides a method to update re-encrypted cipher data - /// Provides a method to update re-encrypted folder data - /// Provides a method to update re-encrypted send data - /// Provides a method to update re-encrypted emergency access data - /// Logs out user from other devices after successful rotation - /// Provides a password mismatch error if master password hash validation fails - public RotateUserKeyCommand(IUserService userService, IUserRepository userRepository, - ICipherRepository cipherRepository, IFolderRepository folderRepository, ISendRepository sendRepository, - IEmergencyAccessRepository emergencyAccessRepository, IOrganizationUserRepository organizationUserRepository, - IPushNotificationService pushService, IdentityErrorDescriber errors, IWebAuthnCredentialRepository credentialRepository) - { - _userService = userService; - _userRepository = userRepository; - _cipherRepository = cipherRepository; - _folderRepository = folderRepository; - _sendRepository = sendRepository; - _emergencyAccessRepository = emergencyAccessRepository; - _organizationUserRepository = organizationUserRepository; - _pushService = pushService; - _identityErrorDescriber = errors; - _credentialRepository = credentialRepository; - } - - /// - public async Task RotateUserKeyAsync(User user, RotateUserKeyData model) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash)) - { - return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()); - } - - var now = DateTime.UtcNow; - user.RevisionDate = user.AccountRevisionDate = now; - user.LastKeyRotationDate = now; - user.SecurityStamp = Guid.NewGuid().ToString(); - user.Key = model.Key; - user.PrivateKey = model.PrivateKey; - if (model.Ciphers.Any() || model.Folders.Any() || model.Sends.Any() || model.EmergencyAccesses.Any() || - model.OrganizationUsers.Any() || model.WebAuthnKeys.Any()) - { - List saveEncryptedDataActions = new(); - - if (model.Ciphers.Any()) - { - saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, model.Ciphers)); - } - - if (model.Folders.Any()) - { - saveEncryptedDataActions.Add(_folderRepository.UpdateForKeyRotation(user.Id, model.Folders)); - } - - if (model.Sends.Any()) - { - saveEncryptedDataActions.Add(_sendRepository.UpdateForKeyRotation(user.Id, model.Sends)); - } - - if (model.EmergencyAccesses.Any()) - { - saveEncryptedDataActions.Add( - _emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccesses)); - } - - if (model.OrganizationUsers.Any()) - { - saveEncryptedDataActions.Add( - _organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers)); - } - - if (model.WebAuthnKeys.Any()) - { - saveEncryptedDataActions.Add(_credentialRepository.UpdateKeysForRotationAsync(user.Id, model.WebAuthnKeys)); - } - - await _userRepository.UpdateUserKeyAndEncryptedDataAsync(user, saveEncryptedDataActions); - } - else - { - await _userRepository.ReplaceAsync(user); - } - - await _pushService.PushLogOutAsync(user.Id, excludeCurrentContextFromPush: true); - return IdentityResult.Success; - } -} diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index 581a7e8f04..64261ede82 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -1,27 +1,16 @@ using System.Security.Claims; -using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Auth.Controllers; -using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request.Accounts; -using Bit.Api.Auth.Models.Request.WebAuthn; -using Bit.Api.KeyManagement.Validators; -using Bit.Api.Tools.Models.Request; -using Bit.Api.Vault.Models.Request; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; -using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Api.Request.Accounts; -using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; 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.UserKey; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Entities; -using Bit.Core.Vault.Entities; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Identity; using NSubstitute; @@ -39,23 +28,10 @@ public class AccountsControllerTests : IDisposable private readonly IProviderUserRepository _providerUserRepository; private readonly IPolicyService _policyService; private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand; - private readonly IRotateUserKeyCommand _rotateUserKeyCommand; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; private readonly IFeatureService _featureService; - private readonly IRotationValidator, IEnumerable> _cipherValidator; - private readonly IRotationValidator, IEnumerable> _folderValidator; - private readonly IRotationValidator, IReadOnlyList> _sendValidator; - private readonly IRotationValidator, IEnumerable> - _emergencyAccessValidator; - private readonly IRotationValidator, - IReadOnlyList> - _resetPasswordValidator; - private readonly IRotationValidator, IEnumerable> - _webauthnKeyRotationValidator; - - public AccountsControllerTests() { _userService = Substitute.For(); @@ -64,21 +40,9 @@ public class AccountsControllerTests : IDisposable _providerUserRepository = Substitute.For(); _policyService = Substitute.For(); _setInitialMasterPasswordCommand = Substitute.For(); - _rotateUserKeyCommand = Substitute.For(); _twoFactorIsEnabledQuery = Substitute.For(); _tdeOffboardingPasswordCommand = Substitute.For(); _featureService = Substitute.For(); - _cipherValidator = - Substitute.For, IEnumerable>>(); - _folderValidator = - Substitute.For, IEnumerable>>(); - _sendValidator = Substitute.For, IReadOnlyList>>(); - _emergencyAccessValidator = Substitute.For, - IEnumerable>>(); - _webauthnKeyRotationValidator = Substitute.For, IEnumerable>>(); - _resetPasswordValidator = Substitute - .For, - IReadOnlyList>>(); _sut = new AccountsController( _organizationService, @@ -88,15 +52,8 @@ public class AccountsControllerTests : IDisposable _policyService, _setInitialMasterPasswordCommand, _tdeOffboardingPasswordCommand, - _rotateUserKeyCommand, _twoFactorIsEnabledQuery, - _featureService, - _cipherValidator, - _folderValidator, - _sendValidator, - _emergencyAccessValidator, - _resetPasswordValidator, - _webauthnKeyRotationValidator + _featureService ); } diff --git a/test/Core.Test/KeyManagement/UserKey/RotateUserKeyCommandTests.cs b/test/Core.Test/KeyManagement/UserKey/RotateUserKeyCommandTests.cs deleted file mode 100644 index 000fa7e90c..0000000000 --- a/test/Core.Test/KeyManagement/UserKey/RotateUserKeyCommandTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Bit.Core.Auth.Entities; -using Bit.Core.Auth.Repositories; -using Bit.Core.Entities; -using Bit.Core.KeyManagement.Models.Data; -using Bit.Core.KeyManagement.UserKey.Implementations; -using Bit.Core.Platform.Push; -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.UserKey; - -[SutProviderCustomize] -public class RotateUserKeyCommandTests -{ - [Theory, BitAutoData] - public async Task RotateUserKeyAsync_Success(SutProvider sutProvider, User user, - RotateUserKeyData model) - { - sutProvider.GetDependency().CheckPasswordAsync(user, model.MasterPasswordHash) - .Returns(true); - foreach (var webauthnCred in model.WebAuthnKeys) - { - var dbWebauthnCred = new WebAuthnCredential - { - EncryptedPublicKey = "encryptedPublicKey", - EncryptedUserKey = "encryptedUserKey" - }; - sutProvider.GetDependency().GetByIdAsync(webauthnCred.Id, user.Id) - .Returns(dbWebauthnCred); - } - - var result = await sutProvider.Sut.RotateUserKeyAsync(user, model); - - Assert.Equal(IdentityResult.Success, result); - } - - [Theory, BitAutoData] - public async Task RotateUserKeyAsync_InvalidMasterPasswordHash_ReturnsFailedIdentityResult( - SutProvider sutProvider, User user, RotateUserKeyData model) - { - sutProvider.GetDependency().CheckPasswordAsync(user, model.MasterPasswordHash) - .Returns(false); - foreach (var webauthnCred in model.WebAuthnKeys) - { - var dbWebauthnCred = new WebAuthnCredential - { - EncryptedPublicKey = "encryptedPublicKey", - EncryptedUserKey = "encryptedUserKey" - }; - sutProvider.GetDependency().GetByIdAsync(webauthnCred.Id, user.Id) - .Returns(dbWebauthnCred); - } - - var result = await sutProvider.Sut.RotateUserKeyAsync(user, model); - - Assert.False(result.Succeeded); - } - - [Theory, BitAutoData] - public async Task RotateUserKeyAsync_LogsOutUser( - SutProvider sutProvider, User user, RotateUserKeyData model) - { - sutProvider.GetDependency().CheckPasswordAsync(user, model.MasterPasswordHash) - .Returns(true); - foreach (var webauthnCred in model.WebAuthnKeys) - { - var dbWebauthnCred = new WebAuthnCredential - { - EncryptedPublicKey = "encryptedPublicKey", - EncryptedUserKey = "encryptedUserKey" - }; - sutProvider.GetDependency().GetByIdAsync(webauthnCred.Id, user.Id) - .Returns(dbWebauthnCred); - } - - await sutProvider.Sut.RotateUserKeyAsync(user, model); - - await sutProvider.GetDependency().ReceivedWithAnyArgs() - .PushLogOutAsync(default, default); - } - -} From d33b0ce1cfdf298491b406ff13ea4a5cec2b9dc9 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:58:43 +0100 Subject: [PATCH 25/47] [PM 18208]Cancel self-serve trials with no payment method (#5916) * cancel self-serve trials with no payment method * Remove unnecessary code block * Update the invoice create for missing paymentMethod * Remove the create_invoice implementation * Remove empty spaces --- .../Implementations/OrganizationBillingService.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index 32521f00c8..c67aa21971 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -426,6 +426,19 @@ public class OrganizationBillingService( TrialPeriodDays = subscriptionSetup.SkipTrial ? 0 : plan.TrialPeriodDays }; + // Only set trial_settings.end_behavior.missing_payment_method to "cancel" if there is no payment method + if (string.IsNullOrEmpty(customer.InvoiceSettings?.DefaultPaymentMethodId) && + !customer.Metadata.ContainsKey(BraintreeCustomerIdKey)) + { + subscriptionCreateOptions.TrialSettings = new SubscriptionTrialSettingsOptions + { + EndBehavior = new SubscriptionTrialSettingsEndBehaviorOptions + { + MissingPaymentMethod = "cancel" + } + }; + } + var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); From 51e93c7323ad6aaefe23022b82091ffccfa71630 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:02:07 -0500 Subject: [PATCH 26/47] add end user activation feature flag (#5989) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 0bd8baa85e..9438a23f9d 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -208,6 +208,7 @@ public static class FeatureFlagKeys public const string PhishingDetection = "phishing-detection"; 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 static List GetAllKeys() { From 74964bf170991f56f36bffeeb19ee844a18fe588 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Tue, 24 Jun 2025 14:53:04 -0500 Subject: [PATCH 27/47] [PM-20577] OrganizationReport endpoints (#5986) * PM-20574 fixing namespaces on reporting work that got moved over from tools * PM-20574 adding tables, stored procedures, and migration files * PM-20574 adding dapper and ef repos and migrations * PM-20574 changing table and repo names as requested * PM-20574 updating sql scripts to new names * PM-20574 updating sql scripts * PM-20574 updating migration script for org delete by id * PM-20574 adding mysql migration * PM-20574 updating sql migration to fix database test * PM-20574 fixing migration script * PM-20574 fixing migration script * PM-20574 fixing table scripts * PM-20574 fixing table scripts * PM-20574 fixing migration script formatting * PM-20574 fixing syntax in migration script * PM-20574 fixing file names and extensions * PM-20574 fixing sql file * PM-20574 fixing sql * PM-20574 fixing directory for entities and removing scripts from other databases * PM-20574 generating new migration scripts * PM-20574 fixed reference to a stored proc * PM-20574 adding index in scripts and missing table * PM-20574 fixing merge conflicts * PM-20574 set OUTPUT param for Id property in create and update proc * PM-20574 add CreateDate to the update proc * PM-20574 amend update proc for OrganizationApplication by adding createDate * PM-20576 Created OrganizationReportRepo and unit tests * PM-20576 Commands and Query for OrganizationReport * PM-20576 added additional unit tests to fix CodeCoverage report * PM-20574 formatted sql and updated as per PR comments * PM-20574 updated script to fix build error * PM-20574 fixed inconsistency in db script * PM-20577 organization-reports endpoints * PM-20574 removed revisionDate, update procedures and used views * PM-20574 removed RevisionDate from designer files * PM-20574 removed revisionDate column that was missed previously * PM-20574 added revision date back into the mix * PM-20574 updated database script to fix build error * PM-20574 fixed a procedure issue * PM-20574 fix dB build error * PM-020574 fixed additional PR comments - files cleaned up * PM-20574 updated procedure was inconsistent * PM-20576 added logs and updated errors as per PR comments * PM-20576 fixed a build error * PM-20576 removed RevisionDate from Repo and tests * PM-20576 added dependency * PM-20576 removed unwanted line from csproj file --------- Co-authored-by: Graham Walker Co-authored-by: Tom <144813356+ttalty@users.noreply.github.com> --- src/Api/Dirt/Controllers/ReportsController.cs | 79 +++++++++- .../DropOrganizationReportCommand.cs | 15 +- test/Api.Test/Dirt/ReportsControllerTests.cs | 138 ++++++++++++++++++ 3 files changed, 225 insertions(+), 7 deletions(-) diff --git a/src/Api/Dirt/Controllers/ReportsController.cs b/src/Api/Dirt/Controllers/ReportsController.cs index 4f2697dcc5..8bb8b5e487 100644 --- a/src/Api/Dirt/Controllers/ReportsController.cs +++ b/src/Api/Dirt/Controllers/ReportsController.cs @@ -23,6 +23,9 @@ public class ReportsController : Controller private readonly IAddPasswordHealthReportApplicationCommand _addPwdHealthReportAppCommand; private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery; private readonly IDropPasswordHealthReportApplicationCommand _dropPwdHealthReportAppCommand; + private readonly IAddOrganizationReportCommand _addOrganizationReportCommand; + private readonly IDropOrganizationReportCommand _dropOrganizationReportCommand; + private readonly IGetOrganizationReportQuery _getOrganizationReportQuery; public ReportsController( ICurrentContext currentContext, @@ -30,7 +33,10 @@ public class ReportsController : Controller IRiskInsightsReportQuery riskInsightsReportQuery, IAddPasswordHealthReportApplicationCommand addPasswordHealthReportApplicationCommand, IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery, - IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand + IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand, + IGetOrganizationReportQuery getOrganizationReportQuery, + IAddOrganizationReportCommand addOrganizationReportCommand, + IDropOrganizationReportCommand dropOrganizationReportCommand ) { _currentContext = currentContext; @@ -39,6 +45,9 @@ public class ReportsController : Controller _addPwdHealthReportAppCommand = addPasswordHealthReportApplicationCommand; _getPwdHealthReportAppQuery = getPasswordHealthReportApplicationQuery; _dropPwdHealthReportAppCommand = dropPwdHealthReportAppCommand; + _getOrganizationReportQuery = getOrganizationReportQuery; + _addOrganizationReportCommand = addOrganizationReportCommand; + _dropOrganizationReportCommand = dropOrganizationReportCommand; } /// @@ -204,4 +213,72 @@ 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); + } } diff --git a/src/Core/Dirt/Reports/ReportFeatures/DropOrganizationReportCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/DropOrganizationReportCommand.cs index e382788273..8fe206c1f1 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/DropOrganizationReportCommand.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/DropOrganizationReportCommand.cs @@ -31,12 +31,15 @@ public class DropOrganizationReportCommand : IDropOrganizationReportCommand throw new BadRequestException("No data found."); } - data.Where(_ => request.OrganizationReportIds.Contains(_.Id)).ToList().ForEach(async _ => - { - _logger.LogInformation("Dropping organization report {organizationReportId} for organization {organizationId}", - _.Id, request.OrganizationId); + 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(_); - }); + await _organizationReportRepo.DeleteAsync(reportId); + }); } } diff --git a/test/Api.Test/Dirt/ReportsControllerTests.cs b/test/Api.Test/Dirt/ReportsControllerTests.cs index 37a6cb79c3..af285d8b85 100644 --- a/test/Api.Test/Dirt/ReportsControllerTests.cs +++ b/test/Api.Test/Dirt/ReportsControllerTests.cs @@ -142,4 +142,142 @@ 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); + } } From 2af4e9ccfa7c3848ca9f4b82adf9dad01268736f Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Wed, 25 Jun 2025 09:11:19 -0400 Subject: [PATCH 28/47] [PM-22812] [PM-22985] Attachments get corrupted when downgrading from cipherkeys (#5998) * Reverted change that didn't include the attachments when updating * Made change to cipher_update --- .../Cipher/CipherDetails_Update.sql | 3 +- .../Cipher/Cipher_Update.sql | 3 +- ...06-24_00_AttachmentCipherUpdateDetails.sql | 118 ++++++++++++++++++ 3 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-06-24_00_AttachmentCipherUpdateDetails.sql 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 214b74d092..8fc95eb302 100644 --- a/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_Update.sql +++ b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_Update.sql @@ -6,7 +6,7 @@ @Data NVARCHAR(MAX), @Favorites NVARCHAR(MAX), -- not used @Folders NVARCHAR(MAX), -- not used - @Attachments NVARCHAR(MAX), -- not used + @Attachments NVARCHAR(MAX), @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7), @FolderId UNIQUEIDENTIFIER, @@ -50,6 +50,7 @@ BEGIN ELSE JSON_MODIFY([Favorites], @UserIdPath, NULL) END, + [Attachments] = @Attachments, [Reprompt] = @Reprompt, [CreationDate] = @CreationDate, [RevisionDate] = @RevisionDate, 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 0aa73ae9b6..8baf1b5f0f 100644 --- a/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Update.sql +++ b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Update.sql @@ -6,7 +6,7 @@ @Data NVARCHAR(MAX), @Favorites NVARCHAR(MAX), @Folders NVARCHAR(MAX), - @Attachments NVARCHAR(MAX), -- not used + @Attachments NVARCHAR(MAX), @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7), @DeletedDate DATETIME2(7), @@ -25,6 +25,7 @@ BEGIN [Data] = @Data, [Favorites] = @Favorites, [Folders] = @Folders, + [Attachments] = @Attachments, [CreationDate] = @CreationDate, [RevisionDate] = @RevisionDate, [DeletedDate] = @DeletedDate, diff --git a/util/Migrator/DbScripts/2025-06-24_00_AttachmentCipherUpdateDetails.sql b/util/Migrator/DbScripts/2025-06-24_00_AttachmentCipherUpdateDetails.sql new file mode 100644 index 0000000000..1af98dba76 --- /dev/null +++ b/util/Migrator/DbScripts/2025-06-24_00_AttachmentCipherUpdateDetails.sql @@ -0,0 +1,118 @@ +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 +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 + 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].[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 +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 + 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 From 0f12d076ec1c3c07088b093dbca0f0e1dbe99004 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Wed, 25 Jun 2025 09:30:51 -0700 Subject: [PATCH 29/47] fix(grantor-policies): [PM-21921] add null check (#5886) When getting grantor policies, adds a null check so that: - For a Grantor who is an org Owner, we respond with a `200` and the policies - For a Grantor is not an org Owner, we respond with a `200` and `null` --- src/Api/Auth/Controllers/EmergencyAccessController.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Api/Auth/Controllers/EmergencyAccessController.cs b/src/Api/Auth/Controllers/EmergencyAccessController.cs index 5d1f47de73..8b40444634 100644 --- a/src/Api/Auth/Controllers/EmergencyAccessController.cs +++ b/src/Api/Auth/Controllers/EmergencyAccessController.cs @@ -4,7 +4,6 @@ using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Response; using Bit.Api.Models.Response; using Bit.Api.Vault.Models.Response; -using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Services; using Bit.Core.Exceptions; using Bit.Core.Repositories; @@ -72,7 +71,7 @@ public class EmergencyAccessController : Controller { var user = await _userService.GetUserByPrincipalAsync(User); var policies = await _emergencyAccessService.GetPoliciesAsync(id, user); - var responses = policies.Select(policy => new PolicyResponseModel(policy)); + var responses = policies?.Select(policy => new PolicyResponseModel(policy)); return new ListResponseModel(responses); } From 5d9071da1f4bce178505b692d57ff6a4aaeae0c9 Mon Sep 17 00:00:00 2001 From: tangowithfoxtrot <5676771+tangowithfoxtrot@users.noreply.github.com> Date: Wed, 25 Jun 2025 10:20:42 -0700 Subject: [PATCH 30/47] fix: failed db migrations due to missing kerberos tools (#6005) --- src/Admin/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Admin/Dockerfile b/src/Admin/Dockerfile index d6b42eadfb..0d6fd4cc78 100644 --- a/src/Admin/Dockerfile +++ b/src/Admin/Dockerfile @@ -59,6 +59,7 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends \ gosu \ curl \ + krb5-user \ && rm -rf /var/lib/apt/lists/* # Copy app from the build stage From 71aae24246f1a68b3481c188ee0a7242167001cb Mon Sep 17 00:00:00 2001 From: Andy Pixley <3723676+pixman20@users.noreply.github.com> Date: Wed, 25 Jun 2025 14:39:35 -0400 Subject: [PATCH 31/47] [BRE-848] Fixing release workflow permissions (#6002) --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a9cc2d966..c62587fe39 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,6 +59,8 @@ jobs: name: Create GitHub release runs-on: ubuntu-22.04 needs: setup + permissions: + contents: write steps: - name: Download latest release Docker stubs if: ${{ inputs.release_type != 'Dry Run' }} From 49816e0926c7511705ef2080cdf4675f8d17f27f Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:17:35 +0200 Subject: [PATCH 32/47] Remove unused feature flag "item-share" (#6003) Co-authored-by: Daniel James Smith --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 9438a23f9d..066d73f6d1 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -194,7 +194,6 @@ public static class FeatureFlagKeys public const string IpcChannelFramework = "ipc-channel-framework"; /* Tools Team */ - public const string ItemShare = "item-share"; public const string DesktopSendUIRefresh = "desktop-send-ui-refresh"; /* Vault Team */ From 7fd1ccb7a2ff57ae07808ef44b893c00749a76de Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 26 Jun 2025 13:38:14 +0100 Subject: [PATCH 33/47] [PM-18699] Add trial path to Stripe metadata (#5940) * Add the market and product initiated change * Resolve the failing test * revert the change * resolve the failing test * Resolve the failing test * revert the add skipstrial * Revert add the new bool SkipTrial * Revert the changes * Revert the changes on the organizationsale * remove the trailsource property * Remove the CustomerSetup added * Add the improved code change for trial metadata * Revert the changes on GetSubscriptionSetup * Assign the InitiationPath --- src/Core/Billing/Models/Sales/OrganizationSale.cs | 1 + src/Core/Billing/Models/Sales/SubscriptionSetup.cs | 1 + .../Services/Implementations/OrganizationBillingService.cs | 6 +++++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Core/Billing/Models/Sales/OrganizationSale.cs b/src/Core/Billing/Models/Sales/OrganizationSale.cs index 78ad26871b..c8ccb0f9a1 100644 --- a/src/Core/Billing/Models/Sales/OrganizationSale.cs +++ b/src/Core/Billing/Models/Sales/OrganizationSale.cs @@ -34,6 +34,7 @@ public class OrganizationSale var subscriptionSetup = GetSubscriptionSetup(signup); subscriptionSetup.SkipTrial = signup.SkipTrial; + subscriptionSetup.InitiationPath = signup.InitiationPath; return new OrganizationSale { diff --git a/src/Core/Billing/Models/Sales/SubscriptionSetup.cs b/src/Core/Billing/Models/Sales/SubscriptionSetup.cs index 871a2920b1..5e891f75b6 100644 --- a/src/Core/Billing/Models/Sales/SubscriptionSetup.cs +++ b/src/Core/Billing/Models/Sales/SubscriptionSetup.cs @@ -10,6 +10,7 @@ public class SubscriptionSetup public required PasswordManager PasswordManagerOptions { get; set; } public SecretsManager? SecretsManagerOptions { get; set; } public bool SkipTrial = false; + public string? InitiationPath { get; set; } public class PasswordManager { diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index c67aa21971..725e274fa2 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -420,7 +420,11 @@ public class OrganizationBillingService( Items = subscriptionItemOptionsList, Metadata = new Dictionary { - ["organizationId"] = organizationId.ToString() + ["organizationId"] = organizationId.ToString(), + ["trialInitiationPath"] = !string.IsNullOrEmpty(subscriptionSetup.InitiationPath) && + subscriptionSetup.InitiationPath.Contains("trial from marketing website") + ? "marketing-initiated" + : "product-initiated" }, OffSession = true, TrialPeriodDays = subscriptionSetup.SkipTrial ? 0 : plan.TrialPeriodDays From b418b07f26ce27434dd3e63e194f746d37da80db Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Thu, 26 Jun 2025 09:19:49 -0400 Subject: [PATCH 34/47] [PM-17562] Add support for Auth on Webhook integration requests (#5970) * [PM-17562] Update documentation for event integrations * Fix SonarQube suggestion, bring ASB event listener in line with integration listener * Apply suggestions from code review Co-authored-by: Matt Bishop * Updates to README - PR fixes, additional context, tense alignment * Add links to different sections; remove inline code formatting in favor of single bacticks for JSON * [PM-17562] Add aupport for Auth on Webhook integration requests * Repsond to PR feedback - move optional params to end, add tests for optional cases --------- Co-authored-by: Matt Bishop --- .../EventIntegrations/SlackIntegration.cs | 2 +- .../SlackIntegrationConfiguration.cs | 2 +- .../SlackIntegrationConfigurationDetails.cs | 2 +- .../WebhookIntegrationConfiguration.cs | 2 +- .../WebhookIntegrationConfigurationDetails.cs | 2 +- .../SlackIntegrationHandler.cs | 4 +- .../WebhookIntegrationHandler.cs | 13 ++- ...ntegrationsConfigurationControllerTests.cs | 93 +++++++++++++++++-- ...tegrationConfigurationRequestModelTests.cs | 17 +++- .../IntegrationMessageTests.cs | 4 +- .../Services/EventIntegrationHandlerTests.cs | 6 +- .../Services/IntegrationHandlerTests.cs | 2 +- .../WebhookIntegrationHandlerTests.cs | 40 +++++++- 13 files changed, 160 insertions(+), 29 deletions(-) diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs index 1e6fbadb34..e8bfaee303 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs @@ -2,4 +2,4 @@ namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; -public record SlackIntegration(string token); +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 13d1ad168f..2c757aeb76 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs @@ -2,4 +2,4 @@ namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; -public record SlackIntegrationConfiguration(string channelId); +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 219149e4f9..6c3d4c2fff 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs @@ -2,4 +2,4 @@ namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; -public record SlackIntegrationConfigurationDetails(string channelId, string token); +public record SlackIntegrationConfigurationDetails(string ChannelId, string Token); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs index 5d00778e4b..ff28edc301 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); +public record WebhookIntegrationConfiguration(string Url, 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 4790588c32..e0ed5dfcfa 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); +public record WebhookIntegrationConfigurationDetails(string Url, string? Scheme = null, string? Token = null); diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs index f32d1166fa..6f55c0cf9c 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs @@ -11,9 +11,9 @@ public class SlackIntegrationHandler( public override async Task HandleAsync(IntegrationMessage message) { await slackService.SendSlackMessageByChannelIdAsync( - message.Configuration.token, + message.Configuration.Token, message.RenderedTemplate, - message.Configuration.channelId + message.Configuration.ChannelId ); return new IntegrationHandlerResult(success: true, message: message); diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs index 6dc348310d..b66df59a69 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Net; +using System.Net.Http.Headers; using System.Text; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; @@ -20,8 +21,16 @@ public class WebhookIntegrationHandler( public override async Task HandleAsync(IntegrationMessage message) { - var content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json"); - var response = await _httpClient.PostAsync(message.Configuration.url, content); + var request = new HttpRequestMessage(HttpMethod.Post, message.Configuration.Url); + request.Content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json"); + if (!string.IsNullOrEmpty(message.Configuration.Scheme)) + { + request.Headers.Authorization = new AuthenticationHeaderValue( + scheme: message.Configuration.Scheme, + parameter: message.Configuration.Token + ); + } + var response = await _httpClient.SendAsync(request); var result = new IntegrationHandlerResult(success: response.IsSuccessStatusCode, message); switch (response.StatusCode) diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs index c72da2b16f..77283475cf 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs @@ -151,7 +151,7 @@ public class OrganizationIntegrationsConfigurationControllerTests { organizationIntegration.OrganizationId = organizationId; organizationIntegration.Type = IntegrationType.Slack; - var slackConfig = new SlackIntegrationConfiguration(channelId: "C123456"); + var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456"); model.Configuration = JsonSerializer.Serialize(slackConfig); model.Template = "Template String"; @@ -188,7 +188,44 @@ public class OrganizationIntegrationsConfigurationControllerTests { organizationIntegration.OrganizationId = organizationId; organizationIntegration.Type = IntegrationType.Webhook; - var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost"); + var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN"); + model.Configuration = JsonSerializer.Serialize(webhookConfig); + model.Template = "Template String"; + + var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration); + + sutProvider.Sut.Url = Substitute.For(); + sutProvider.GetDependency() + .OrganizationOwner(organizationId) + .Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(organizationIntegration); + sutProvider.GetDependency() + .CreateAsync(Arg.Any()) + .Returns(organizationIntegrationConfiguration); + var requestAction = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model); + + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Any()); + Assert.IsType(requestAction); + Assert.Equal(expected.Id, requestAction.Id); + Assert.Equal(expected.Configuration, requestAction.Configuration); + Assert.Equal(expected.EventType, requestAction.EventType); + Assert.Equal(expected.Template, requestAction.Template); + } + + [Theory, BitAutoData] + public async Task PostAsync_OnlyUrlProvided_Webhook_Succeeds( + SutProvider sutProvider, + Guid organizationId, + OrganizationIntegration organizationIntegration, + OrganizationIntegrationConfiguration organizationIntegrationConfiguration, + OrganizationIntegrationConfigurationRequestModel model) + { + organizationIntegration.OrganizationId = organizationId; + organizationIntegration.Type = IntegrationType.Webhook; + var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost"); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = "Template String"; @@ -350,7 +387,7 @@ public class OrganizationIntegrationsConfigurationControllerTests { organizationIntegration.OrganizationId = organizationId; organizationIntegration.Type = IntegrationType.Webhook; - var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost"); + var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN"); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = null; @@ -393,7 +430,7 @@ public class OrganizationIntegrationsConfigurationControllerTests organizationIntegration.OrganizationId = organizationId; organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id; organizationIntegration.Type = IntegrationType.Slack; - var slackConfig = new SlackIntegrationConfiguration(channelId: "C123456"); + var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456"); model.Configuration = JsonSerializer.Serialize(slackConfig); model.Template = "Template String"; @@ -436,7 +473,49 @@ 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(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN"); + model.Configuration = JsonSerializer.Serialize(webhookConfig); + model.Template = "Template String"; + + var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration)); + + sutProvider.Sut.Url = Substitute.For(); + sutProvider.GetDependency() + .OrganizationOwner(organizationId) + .Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(organizationIntegration); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(organizationIntegrationConfiguration); + var requestAction = await sutProvider.Sut.UpdateAsync( + organizationId, + organizationIntegration.Id, + organizationIntegrationConfiguration.Id, + model); + + await sutProvider.GetDependency().Received(1) + .ReplaceAsync(Arg.Any()); + Assert.IsType(requestAction); + Assert.Equal(expected.Id, requestAction.Id); + Assert.Equal(expected.Configuration, requestAction.Configuration); + Assert.Equal(expected.EventType, requestAction.EventType); + Assert.Equal(expected.Template, requestAction.Template); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_OnlyUrlProvided_Webhook_Succeeds( + SutProvider sutProvider, + Guid organizationId, + OrganizationIntegration organizationIntegration, + OrganizationIntegrationConfiguration organizationIntegrationConfiguration, + OrganizationIntegrationConfigurationRequestModel model) + { + organizationIntegration.OrganizationId = organizationId; + organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id; + organizationIntegration.Type = IntegrationType.Webhook; + var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost"); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = "Template String"; @@ -476,7 +555,7 @@ public class OrganizationIntegrationsConfigurationControllerTests { organizationIntegration.OrganizationId = organizationId; organizationIntegration.Type = IntegrationType.Webhook; - var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost"); + var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN"); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = "Template String"; @@ -582,7 +661,7 @@ public class OrganizationIntegrationsConfigurationControllerTests organizationIntegration.OrganizationId = organizationId; organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id; organizationIntegration.Type = IntegrationType.Slack; - var slackConfig = new SlackIntegrationConfiguration(channelId: "C123456"); + var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456"); model.Configuration = JsonSerializer.Serialize(slackConfig); model.Template = null; diff --git a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs index 19ae20804b..5958aa06aa 100644 --- a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs +++ b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs @@ -43,7 +43,7 @@ public class OrganizationIntegrationConfigurationRequestModelTests [InlineData(" ")] public void IsValidForType_EmptyTemplate_ReturnsFalse(string? template) { - var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com")); + var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN")); var model = new OrganizationIntegrationConfigurationRequestModel { Configuration = config, @@ -92,7 +92,7 @@ public class OrganizationIntegrationConfigurationRequestModelTests } [Fact] - public void IsValidForType_ValidWebhookConfiguration_ReturnsTrue() + public void IsValidForType_ValidNoAuthWebhookConfiguration_ReturnsTrue() { var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com")); var model = new OrganizationIntegrationConfigurationRequestModel @@ -104,6 +104,19 @@ public class OrganizationIntegrationConfigurationRequestModelTests Assert.True(model.IsValidForType(IntegrationType.Webhook)); } + [Fact] + public void IsValidForType_ValidWebhookConfiguration_ReturnsTrue() + { + var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN")); + var model = new OrganizationIntegrationConfigurationRequestModel + { + Configuration = config, + Template = "template" + }; + + Assert.True(model.IsValidForType(IntegrationType.Webhook)); + } + [Fact] public void IsValidForType_UnknownIntegrationType_ReturnsFalse() { diff --git a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs index ebb6fa079e..a68bfd4fcb 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"), + Configuration = new WebhookIntegrationConfigurationDetails("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"), + Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"), MessageId = _messageId, RenderedTemplate = "This is the message", IntegrationType = IntegrationType.Webhook, diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs index d4ceabae75..8dcc49a8f1 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs @@ -62,7 +62,7 @@ public class EventIntegrationHandlerTests { var config = Substitute.For(); config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url }); + config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url }); config.Template = template; return [config]; @@ -72,11 +72,11 @@ public class EventIntegrationHandlerTests { var config = Substitute.For(); config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url }); + config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url }); config.Template = template; var config2 = Substitute.For(); config2.Configuration = null; - config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url2 }); + config2.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url2 }); config2.Template = template; return [config, config2]; diff --git a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs index 7f113cfe53..b4a384d798 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"), + Configuration = new WebhookIntegrationConfigurationDetails("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 676a975b77..9a03fb28f0 100644 --- a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http.Headers; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; @@ -16,6 +17,8 @@ public class WebhookIntegrationHandlerTests { private readonly MockedHttpMessageHandler _handler; private readonly HttpClient _httpClient; + private const string _scheme = "Bearer"; + private const string _token = "AUTH_TOKEN"; private const string _webhookUrl = "http://localhost/test/event"; public WebhookIntegrationHandlerTests() @@ -39,7 +42,7 @@ public class WebhookIntegrationHandlerTests } [Theory, BitAutoData] - public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage message) + public async Task HandleAsync_SuccessfulRequestWithoutAuth_ReturnsSuccess(IntegrationMessage message) { var sutProvider = GetSutProvider(); message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); @@ -59,6 +62,33 @@ public class WebhookIntegrationHandlerTests var returned = await request.Content.ReadAsStringAsync(); Assert.Equal(HttpMethod.Post, request.Method); + Assert.Null(request.Headers.Authorization); + Assert.Equal(_webhookUrl, request.RequestUri.ToString()); + AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned); + } + + [Theory, BitAutoData] + public async Task HandleAsync_SuccessfulRequestWithAuthorizationHeader_ReturnsSuccess(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.True(result.Success); + Assert.Equal(result.Message, message); + + sutProvider.GetDependency().Received(1).CreateClient( + Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName)) + ); + + Assert.Single(_handler.CapturedRequests); + var request = _handler.CapturedRequests[0]; + Assert.NotNull(request); + var returned = await request.Content.ReadAsStringAsync(); + + Assert.Equal(HttpMethod.Post, request.Method); + Assert.Equal(new AuthenticationHeaderValue(_scheme, _token), request.Headers.Authorization); Assert.Equal(_webhookUrl, request.RequestUri.ToString()); AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned); } @@ -71,7 +101,7 @@ public class WebhookIntegrationHandlerTests var retryAfter = now.AddSeconds(60); sutProvider.GetDependency().SetUtcNow(now); - message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token); _handler.Fallback .WithStatusCode(HttpStatusCode.TooManyRequests) @@ -94,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); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token); _handler.Fallback .WithStatusCode(HttpStatusCode.TooManyRequests) @@ -115,7 +145,7 @@ public class WebhookIntegrationHandlerTests public async Task HandleAsync_InternalServerError_ReturnsFailureSetsRetryable(IntegrationMessage message) { var sutProvider = GetSutProvider(); - message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token); _handler.Fallback .WithStatusCode(HttpStatusCode.InternalServerError) @@ -134,7 +164,7 @@ public class WebhookIntegrationHandlerTests public async Task HandleAsync_UnexpectedRedirect_ReturnsFailureNotRetryable(IntegrationMessage message) { var sutProvider = GetSutProvider(); - message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token); _handler.Fallback .WithStatusCode(HttpStatusCode.TemporaryRedirect) From 82aa47ad4245a81df6457533614bcee7a875c299 Mon Sep 17 00:00:00 2001 From: tangowithfoxtrot <5676771+tangowithfoxtrot@users.noreply.github.com> Date: Thu, 26 Jun 2025 07:49:30 -0700 Subject: [PATCH 35/47] fix(events): re-add kerberos tools to container image (#6007) --- src/Events/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Events/Dockerfile b/src/Events/Dockerfile index 58fbbe3df1..3a6342ef7a 100644 --- a/src/Events/Dockerfile +++ b/src/Events/Dockerfile @@ -50,6 +50,7 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends \ gosu \ curl \ + krb5-user \ && rm -rf /var/lib/apt/lists/* # Copy app from the build stage From 1c3bf259e9fe06d93889ef8c8ee0058bbd9b3b13 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 12:48:27 -0400 Subject: [PATCH 36/47] chore(deps): Platform: Update LaunchDarkly.ServerSdk to 8.9.1 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 88ecaf8cef..e3141c7bfb 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -59,7 +59,7 @@ - + From b951b38c376e5c4c1a537c81be6c935b06040fb2 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:14:05 -0500 Subject: [PATCH 37/47] [PM-21734] Read claims from `ClaimsPrincipal` when creating self-hosted organization (#5953) * Read claims from claims principal when creating self-hosted organization * Run dotnet format * Jared's feedback * Run dotnet format --- .../Implementations/OrganizationService.cs | 242 ++++++++++-------- .../Services/OrganizationFactory.cs | 114 +++++++++ 2 files changed, 243 insertions(+), 113 deletions(-) create mode 100644 src/Core/AdminConsole/Services/OrganizationFactory.cs diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index d5320b5110..7947cbefff 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -97,7 +97,7 @@ public class OrganizationService : IOrganizationService IPricingClient pricingClient, IPolicyRequirementQuery policyRequirementQuery, ISendOrganizationInvitesCommand sendOrganizationInvitesCommand - ) + ) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -198,6 +198,7 @@ public class OrganizationService : IOrganizationService { await AdjustSeatsAsync(organization, seatAdjustment); } + if (maxAutoscaleSeats != organization.MaxAutoscaleSeats) { await UpdateAutoscalingAsync(organization, maxAutoscaleSeats); @@ -206,7 +207,6 @@ public class OrganizationService : IOrganizationService private async Task UpdateAutoscalingAsync(Organization organization, int? maxAutoscaleSeats) { - if (maxAutoscaleSeats.HasValue && organization.Seats.HasValue && maxAutoscaleSeats.Value < organization.Seats.Value) @@ -228,7 +228,8 @@ public class OrganizationService : IOrganizationService if (plan.PasswordManager.MaxSeats.HasValue && maxAutoscaleSeats.HasValue && maxAutoscaleSeats > plan.PasswordManager.MaxSeats) { - throw new BadRequestException(string.Concat($"Your plan has a seat limit of {plan.PasswordManager.MaxSeats}, ", + throw new BadRequestException(string.Concat( + $"Your plan has a seat limit of {plan.PasswordManager.MaxSeats}, ", $"but you have specified a max autoscale count of {maxAutoscaleSeats}.", "Reduce your max autoscale seat count.")); } @@ -249,7 +250,8 @@ public class OrganizationService : IOrganizationService return await AdjustSeatsAsync(organization, seatAdjustment); } - private async Task AdjustSeatsAsync(Organization organization, int seatAdjustment, IEnumerable ownerEmails = null) + private async Task AdjustSeatsAsync(Organization organization, int seatAdjustment, + IEnumerable ownerEmails = null) { if (organization.Seats == null) { @@ -285,10 +287,11 @@ public class OrganizationService : IOrganizationService } var additionalSeats = newSeatTotal - plan.PasswordManager.BaseSeats; - if (plan.PasswordManager.MaxAdditionalSeats.HasValue && additionalSeats > plan.PasswordManager.MaxAdditionalSeats.Value) + if (plan.PasswordManager.MaxAdditionalSeats.HasValue && + additionalSeats > plan.PasswordManager.MaxAdditionalSeats.Value) { throw new BadRequestException($"Organization plan allows a maximum of " + - $"{plan.PasswordManager.MaxAdditionalSeats.Value} additional seats."); + $"{plan.PasswordManager.MaxAdditionalSeats.Value} additional seats."); } if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal) @@ -299,8 +302,9 @@ public class OrganizationService : IOrganizationService { if (organization.UseAdminSponsoredFamilies || seatCounts.Sponsored > 0) { - throw new BadRequestException($"Your organization has {seatCounts.Users} members and {seatCounts.Sponsored} sponsored families. " + - $"To decrease the seat count below {seatCounts.Total}, you must remove members or sponsorships."); + throw new BadRequestException( + $"Your organization has {seatCounts.Users} members and {seatCounts.Sponsored} sponsored families. " + + $"To decrease the seat count below {seatCounts.Total}, you must remove members or sponsorships."); } else { @@ -319,7 +323,8 @@ public class OrganizationService : IOrganizationService organization.Seats = (short?)newSeatTotal; await ReplaceAndUpdateCacheAsync(organization); - if (organization.Seats.HasValue && organization.MaxAutoscaleSeats.HasValue && organization.Seats == organization.MaxAutoscaleSeats) + if (organization.Seats.HasValue && organization.MaxAutoscaleSeats.HasValue && + organization.Seats == organization.MaxAutoscaleSeats) { try { @@ -328,7 +333,9 @@ public class OrganizationService : IOrganizationService ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id, OrganizationUserType.Owner)).Select(u => u.Email).Distinct(); } - await _mailService.SendOrganizationMaxSeatLimitReachedEmailAsync(organization, organization.MaxAutoscaleSeats.Value, ownerEmails); + + await _mailService.SendOrganizationMaxSeatLimitReachedEmailAsync(organization, + organization.MaxAutoscaleSeats.Value, ownerEmails); } catch (Exception e) { @@ -362,7 +369,7 @@ public class OrganizationService : IOrganizationService } var bankAccount = customer.Sources - .FirstOrDefault(s => s is BankAccount && ((BankAccount)s).Status != "verified") as BankAccount; + .FirstOrDefault(s => s is BankAccount && ((BankAccount)s).Status != "verified") as BankAccount; if (bankAccount == null) { throw new GatewayException("Cannot find an unverified bank account."); @@ -389,7 +396,7 @@ public class OrganizationService : IOrganizationService 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."); + "which has a policy that prohibits you from being a member of any other organization."); } } @@ -403,7 +410,7 @@ public class OrganizationService : IOrganizationService 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."); + "Upload this license from your personal account settings page."); } var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license); @@ -422,50 +429,11 @@ public class OrganizationService : IOrganizationService await ValidateSignUpPoliciesAsync(owner.Id); - var organization = new Organization - { - Name = license.Name, - BillingEmail = license.BillingEmail, - BusinessName = license.BusinessName, - PlanType = license.PlanType, - Seats = license.Seats, - MaxCollections = license.MaxCollections, - MaxStorageGb = _globalSettings.SelfHosted ? 10240 : license.MaxStorageGb, // 10 TB - UsePolicies = license.UsePolicies, - UseSso = license.UseSso, - UseKeyConnector = license.UseKeyConnector, - UseScim = license.UseScim, - UseGroups = license.UseGroups, - UseDirectory = license.UseDirectory, - UseEvents = license.UseEvents, - UseTotp = license.UseTotp, - Use2fa = license.Use2fa, - UseApi = license.UseApi, - UseResetPassword = license.UseResetPassword, - Plan = license.Plan, - SelfHost = license.SelfHost, - UsersGetPremium = license.UsersGetPremium, - UseCustomPermissions = license.UseCustomPermissions, - Gateway = null, - GatewayCustomerId = null, - GatewaySubscriptionId = null, - ReferenceData = owner.ReferenceData, - Enabled = license.Enabled, - ExpirationDate = license.Expires, - LicenseKey = license.LicenseKey, - PublicKey = publicKey, - PrivateKey = privateKey, - CreationDate = DateTime.UtcNow, - RevisionDate = DateTime.UtcNow, - Status = OrganizationStatusType.Created, - UsePasswordManager = license.UsePasswordManager, - UseSecretsManager = license.UseSecretsManager, - SmSeats = license.SmSeats, - SmServiceAccounts = license.SmServiceAccounts, - UseRiskInsights = license.UseRiskInsights, - UseOrganizationDomains = license.UseOrganizationDomains, - UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies, - }; + 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); @@ -480,8 +448,9 @@ public class OrganizationService : IOrganizationService /// 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) + private async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> + SignUpAsync(Organization organization, + Guid ownerId, string ownerKey, string collectionName, bool withPayment) { try { @@ -537,7 +506,15 @@ public class OrganizationService : IOrganizationService if (orgUser != null) { defaultOwnerAccess = - [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }]; + [ + new CollectionAccessSelection + { + Id = orgUser.Id, + HidePasswords = false, + ReadOnly = false, + Manage = true + } + ]; } await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess); @@ -573,7 +550,8 @@ 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, + EventType eventType = EventType.Organization_Updated) { if (organization.Id == default(Guid)) { @@ -594,11 +572,12 @@ public class OrganizationService : IOrganizationService if (updateBilling && !string.IsNullOrWhiteSpace(organization.GatewayCustomerId)) { var customerService = new CustomerService(); - await customerService.UpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions - { - Email = organization.BillingEmail, - Description = organization.DisplayBusinessName() - }); + await customerService.UpdateAsync(organization.GatewayCustomerId, + new CustomerUpdateOptions + { + Email = organization.BillingEmail, + Description = organization.DisplayBusinessName() + }); } if (eventType == EventType.Organization_CollectionManagement_Updated) @@ -648,7 +627,8 @@ public class OrganizationService : IOrganizationService await UpdateAsync(organization); } - public async Task InviteUserAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser, + public async Task InviteUserAsync(Guid organizationId, Guid? invitingUserId, + EventSystemUser? systemUser, OrganizationUserInvite invite, string externalId) { // Ideally OrganizationUserInvite should represent a single user so that this doesn't have to be a runtime check @@ -661,7 +641,8 @@ public class OrganizationService : IOrganizationService var invalidAssociations = invite.Collections?.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords)); if (invalidAssociations?.Any() ?? false) { - throw new BadRequestException("The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true."); + throw new BadRequestException( + "The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true."); } var results = await InviteUsersAsync(organizationId, invitingUserId, systemUser, @@ -672,6 +653,7 @@ public class OrganizationService : IOrganizationService { throw new BadRequestException("This user has already been invited."); } + return result; } @@ -683,7 +665,8 @@ public class OrganizationService : IOrganizationService /// The system user which is sending the invite. Only used when inviting via SCIM; null if using a client app or Public API /// Details about the users being invited /// - public async Task> InviteUsersAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser, + public async Task> InviteUsersAsync(Guid organizationId, Guid? invitingUserId, + EventSystemUser? systemUser, IEnumerable<(OrganizationUserInvite invite, string externalId)> invites) { var inviteTypes = new HashSet(invites.Where(i => i.invite.Type.HasValue) @@ -695,7 +678,8 @@ public class OrganizationService : IOrganizationService { foreach (var (invite, _) in invites) { - await ValidateOrganizationUserUpdatePermissions(organizationId, invite.Type.Value, null, invite.Permissions); + await ValidateOrganizationUserUpdatePermissions(organizationId, invite.Type.Value, null, + invite.Permissions); await ValidateOrganizationCustomPermissionsEnabledAsync(organizationId, invite.Type.Value); } } @@ -705,7 +689,8 @@ public class OrganizationService : IOrganizationService if (systemUser.HasValue) { // Log SCIM event - await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.Item1, e.Item2, systemUser.Value, e.Item3))); + await _eventService.LogOrganizationUserEventsAsync(events.Select(e => + (e.Item1, e.Item2, systemUser.Value, e.Item3))); } else { @@ -716,8 +701,10 @@ public class OrganizationService : IOrganizationService return organizationUsers; } - private async Task<(List organizationUsers, List<(OrganizationUser, EventType, DateTime?)> events)> SaveUsersSendInvitesAsync(Guid organizationId, - IEnumerable<(OrganizationUserInvite invite, string externalId)> invites) + private async + Task<(List organizationUsers, List<(OrganizationUser, EventType, DateTime?)> events)> + SaveUsersSendInvitesAsync(Guid organizationId, + IEnumerable<(OrganizationUserInvite invite, string externalId)> invites) { var organization = await GetOrgById(organizationId); var initialSeatCount = organization.Seats; @@ -727,7 +714,8 @@ public class OrganizationService : IOrganizationService } var existingEmails = new HashSet(await _organizationUserRepository.SelectKnownEmailsAsync( - organizationId, invites.SelectMany(i => i.invite.Emails), false), StringComparer.InvariantCultureIgnoreCase); + organizationId, invites.SelectMany(i => i.invite.Emails), false), + StringComparer.InvariantCultureIgnoreCase); // Seat autoscaling var initialSmSeatCount = organization.SmSeats; @@ -755,7 +743,8 @@ public class OrganizationService : IOrganizationService .SelectMany(i => i.invite.Emails) .Count(email => !existingEmails.Contains(email)); - var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organization.Id, inviteWithSmAccessCount); + var additionalSmSeatsRequired = + await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organization.Id, inviteWithSmAccessCount); if (additionalSmSeatsRequired > 0) { var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType); @@ -764,7 +753,9 @@ public class OrganizationService : IOrganizationService } var invitedAreAllOwners = invites.All(i => i.invite.Type == OrganizationUserType.Owner); - if (!invitedAreAllOwners && !await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, new Guid[] { }, includeProvider: true)) + if (!invitedAreAllOwners && + !await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, new Guid[] { }, + includeProvider: true)) { throw new BadRequestException("Organization must have at least one confirmed owner."); } @@ -887,7 +878,8 @@ public class OrganizationService : IOrganizationService await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(smSubscriptionUpdateRevert); } - if (initialSeatCount.HasValue && currentOrganization.Seats.HasValue && currentOrganization.Seats.Value != initialSeatCount.Value) + if (initialSeatCount.HasValue && currentOrganization.Seats.HasValue && + currentOrganization.Seats.Value != initialSeatCount.Value) { await AdjustSeatsAsync(organization, initialSeatCount.Value - currentOrganization.Seats.Value); } @@ -903,7 +895,8 @@ public class OrganizationService : IOrganizationService return (allOrgUsers, events); } - public async Task>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, + public async Task>> ResendInvitesAsync(Guid organizationId, + Guid? invitingUserId, IEnumerable organizationUsersId) { var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId); @@ -925,7 +918,8 @@ public class OrganizationService : IOrganizationService return result; } - public async Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false) + 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 || @@ -1012,7 +1006,9 @@ public class OrganizationService : IOrganizationService IEnumerable ownerEmails; if (providerOrg != null) { - ownerEmails = (await _providerUserRepository.GetManyDetailsByProviderAsync(providerOrg.ProviderId, ProviderUserStatusType.Confirmed)) + ownerEmails = + (await _providerUserRepository.GetManyDetailsByProviderAsync(providerOrg.ProviderId, + ProviderUserStatusType.Confirmed)) .Select(u => u.Email).Distinct(); } else @@ -1020,6 +1016,7 @@ public class OrganizationService : IOrganizationService ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id, OrganizationUserType.Owner)).Select(u => u.Email).Distinct(); } + var initialSeatCount = organization.Seats.Value; await AdjustSeatsAsync(organization, seatsToAdd, ownerEmails); @@ -1034,8 +1031,8 @@ public class OrganizationService : IOrganizationService } - - public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId) + public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, + Guid? callingUserId) { // Org User must be the same as the calling user and the organization ID associated with the user must match passed org ID var orgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId); @@ -1063,30 +1060,35 @@ public class OrganizationService : IOrganizationService // Block the user from withdrawal if auto enrollment is enabled if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) { - var resetPasswordPolicyRequirement = await _policyRequirementQuery.GetAsync(userId); + var resetPasswordPolicyRequirement = + await _policyRequirementQuery.GetAsync(userId); if (resetPasswordKey == null && resetPasswordPolicyRequirement.AutoEnrollEnabled(organizationId)) { - throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to withdraw from account recovery."); + throw new BadRequestException( + "Due to an Enterprise Policy, you are not allowed to withdraw from account recovery."); } - } else { if (resetPasswordKey == null && resetPasswordPolicy.Data != null) { - var data = JsonSerializer.Deserialize(resetPasswordPolicy.Data, JsonHelpers.IgnoreCase); + var data = JsonSerializer.Deserialize(resetPasswordPolicy.Data, + JsonHelpers.IgnoreCase); if (data?.AutoEnrollEnabled ?? false) { - throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to withdraw from account recovery."); + throw new BadRequestException( + "Due to an Enterprise Policy, you are not allowed to withdraw from account recovery."); } } } orgUser.ResetPasswordKey = resetPasswordKey; await _organizationUserRepository.ReplaceAsync(orgUser); - await _eventService.LogOrganizationUserEventAsync(orgUser, resetPasswordKey != null ? - EventType.OrganizationUser_ResetPassword_Enroll : EventType.OrganizationUser_ResetPassword_Withdraw); + await _eventService.LogOrganizationUserEventAsync(orgUser, + resetPasswordKey != null + ? EventType.OrganizationUser_ResetPassword_Enroll + : EventType.OrganizationUser_ResetPassword_Withdraw); } public async Task ImportAsync(Guid organizationId, @@ -1123,15 +1125,16 @@ public class OrganizationService : IOrganizationService 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) + .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 - )) + u, + EventType.OrganizationUser_Removed, + (DateTime?)DateTime.UtcNow + )) ); } @@ -1144,10 +1147,10 @@ public class OrganizationService : IOrganizationService existingExternalUsersIdDict.ContainsKey(u.ExternalId)); await _organizationUserRepository.DeleteManyAsync(usersToDelete.Select(u => u.Id)); events.AddRange(usersToDelete.Select(u => ( - u, - EventType.OrganizationUser_Removed, - (DateTime?)DateTime.UtcNow - )) + u, + EventType.OrganizationUser_Removed, + (DateTime?)DateTime.UtcNow + )) ); foreach (var deletedUser in usersToDelete) { @@ -1175,6 +1178,7 @@ public class OrganizationService : IOrganizationService existingExternalUsersIdDict.Add(orgUser.ExternalId, orgUser.Id); } } + await _organizationUserRepository.UpsertManyAsync(usersToUpsert); // Add new users @@ -1185,7 +1189,8 @@ public class OrganizationService : IOrganizationService var enoughSeatsAvailable = true; if (organization.Seats.HasValue) { - var seatCounts = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); + var seatCounts = + await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); seatsAvailable = organization.Seats.Value - seatCounts.Total; enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count; } @@ -1218,7 +1223,8 @@ public class OrganizationService : IOrganizationService } } - var invitedUsers = await InviteUsersAsync(organizationId, invitingUserId: null, systemUser: eventSystemUser, userInvites); + var invitedUsers = await InviteUsersAsync(organizationId, invitingUserId: null, systemUser: eventSystemUser, + userInvites); foreach (var invitedUser in invitedUsers) { existingExternalUsersIdDict.Add(invitedUser.ExternalId, invitedUser.Id); @@ -1255,7 +1261,8 @@ public class OrganizationService : IOrganizationService } await _eventService.LogGroupEventsAsync( - savedGroups.Select(g => (g, EventType.Group_Created, (EventSystemUser?)eventSystemUser, (DateTime?)DateTime.UtcNow))); + savedGroups.Select(g => (g, EventType.Group_Created, (EventSystemUser?)eventSystemUser, + (DateTime?)DateTime.UtcNow))); var updateGroups = existingExternalGroups .Where(g => groupsDict.ContainsKey(g.ExternalId)) @@ -1282,11 +1289,11 @@ public class OrganizationService : IOrganizationService 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))); + updateGroups.Select(g => (g, EventType.Group_Updated, (EventSystemUser?)eventSystemUser, + (DateTime?)DateTime.UtcNow))); } } @@ -1298,10 +1305,12 @@ public class OrganizationService : IOrganizationService await _ssoUserRepository.DeleteAsync(userId, organizationId); if (organizationId.HasValue) { - var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId.Value, userId); + var organizationUser = + await _organizationUserRepository.GetByOrganizationAsync(organizationId.Value, userId); if (organizationUser != null) { - await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UnlinkedSso); + await _eventService.LogOrganizationUserEventAsync(organizationUser, + EventType.OrganizationUser_UnlinkedSso); } } } @@ -1423,7 +1432,7 @@ public class OrganizationService : IOrganizationService } if ((plan.ProductTier == ProductTierType.TeamsStarter && - upgrade.AdditionalSmSeats.GetValueOrDefault() > plan.PasswordManager.BaseSeats) || + upgrade.AdditionalSmSeats.GetValueOrDefault() > plan.PasswordManager.BaseSeats) || (plan.ProductTier != ProductTierType.TeamsStarter && upgrade.AdditionalSmSeats.GetValueOrDefault() > upgrade.AdditionalSeats)) { @@ -1446,7 +1455,8 @@ public class OrganizationService : IOrganizationService } } - public async Task ValidateOrganizationUserUpdatePermissions(Guid organizationId, OrganizationUserType newType, OrganizationUserType? oldType, Permissions permissions) + public async Task ValidateOrganizationUserUpdatePermissions(Guid organizationId, OrganizationUserType newType, + OrganizationUserType? oldType, Permissions permissions) { if (await _currentContext.OrganizationOwner(organizationId)) { @@ -1473,13 +1483,15 @@ public class OrganizationService : IOrganizationService throw new BadRequestException("Custom users can not manage Admins or Owners."); } - if (newType == OrganizationUserType.Custom && !await ValidateCustomPermissionsGrant(organizationId, permissions)) + if (newType == OrganizationUserType.Custom && + !await ValidateCustomPermissionsGrant(organizationId, permissions)) { throw new BadRequestException("Custom users can only grant the same custom permissions that they have."); } } - public async Task ValidateOrganizationCustomPermissionsEnabledAsync(Guid organizationId, OrganizationUserType newType) + public async Task ValidateOrganizationCustomPermissionsEnabledAsync(Guid organizationId, + OrganizationUserType newType) { if (newType != OrganizationUserType.Custom) { @@ -1494,7 +1506,8 @@ public class OrganizationService : IOrganizationService if (!organization.UseCustomPermissions) { - throw new BadRequestException("To enable custom permissions the organization must be on an Enterprise plan."); + throw new BadRequestException( + "To enable custom permissions the organization must be on an Enterprise plan."); } } @@ -1600,7 +1613,8 @@ public class OrganizationService : IOrganizationService EventSystemUser systemUser) { await RepositoryRevokeUserAsync(organizationUser); - await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, systemUser); + await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, + systemUser); if (organizationUser.UserId.HasValue) { @@ -1615,7 +1629,8 @@ public class OrganizationService : IOrganizationService throw new BadRequestException("Already revoked."); } - if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId, new[] { organizationUser.Id }, includeProvider: true)) + if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId, + new[] { organizationUser.Id }, includeProvider: true)) { throw new BadRequestException("Organization must have at least one confirmed owner."); } @@ -1663,7 +1678,8 @@ public class OrganizationService : IOrganizationService throw new BadRequestException("You cannot revoke yourself."); } - if (organizationUser.Type == OrganizationUserType.Owner && revokingUserId.HasValue && !deletingUserIsOwner) + if (organizationUser.Type == OrganizationUserType.Owner && revokingUserId.HasValue && + !deletingUserIsOwner) { throw new BadRequestException("Only owners can revoke other owners."); } diff --git a/src/Core/AdminConsole/Services/OrganizationFactory.cs b/src/Core/AdminConsole/Services/OrganizationFactory.cs new file mode 100644 index 0000000000..3261d89253 --- /dev/null +++ b/src/Core/AdminConsole/Services/OrganizationFactory.cs @@ -0,0 +1,114 @@ +using System.Security.Claims; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Licenses; +using Bit.Core.Billing.Licenses.Extensions; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Business; + +namespace Bit.Core.AdminConsole.Services; + +public static class OrganizationFactory +{ + public static Organization Create( + User owner, + ClaimsPrincipal claimsPrincipal, + string publicKey, + string privateKey) => new() + { + Name = claimsPrincipal.GetValue(OrganizationLicenseConstants.Name), + BillingEmail = claimsPrincipal.GetValue(OrganizationLicenseConstants.BillingEmail), + BusinessName = claimsPrincipal.GetValue(OrganizationLicenseConstants.BusinessName), + PlanType = claimsPrincipal.GetValue(OrganizationLicenseConstants.PlanType), + Seats = claimsPrincipal.GetValue(OrganizationLicenseConstants.Seats), + MaxCollections = claimsPrincipal.GetValue(OrganizationLicenseConstants.MaxCollections), + MaxStorageGb = 10240, + UsePolicies = claimsPrincipal.GetValue(OrganizationLicenseConstants.UsePolicies), + UseSso = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseSso), + UseKeyConnector = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseKeyConnector), + UseScim = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseScim), + UseGroups = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseGroups), + UseDirectory = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseDirectory), + UseEvents = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseEvents), + UseTotp = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseTotp), + Use2fa = claimsPrincipal.GetValue(OrganizationLicenseConstants.Use2fa), + UseApi = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseApi), + UseResetPassword = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseResetPassword), + Plan = claimsPrincipal.GetValue(OrganizationLicenseConstants.Plan), + SelfHost = claimsPrincipal.GetValue(OrganizationLicenseConstants.SelfHost), + UsersGetPremium = claimsPrincipal.GetValue(OrganizationLicenseConstants.UsersGetPremium), + UseCustomPermissions = + claimsPrincipal.GetValue(OrganizationLicenseConstants.UseCustomPermissions), + Gateway = null, + GatewayCustomerId = null, + GatewaySubscriptionId = null, + ReferenceData = owner.ReferenceData, + Enabled = claimsPrincipal.GetValue(OrganizationLicenseConstants.Enabled), + ExpirationDate = claimsPrincipal.GetValue(OrganizationLicenseConstants.Expires), + LicenseKey = claimsPrincipal.GetValue(OrganizationLicenseConstants.LicenseKey), + PublicKey = publicKey, + PrivateKey = privateKey, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Status = OrganizationStatusType.Created, + UsePasswordManager = claimsPrincipal.GetValue(OrganizationLicenseConstants.UsePasswordManager), + UseSecretsManager = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseSecretsManager), + SmSeats = claimsPrincipal.GetValue(OrganizationLicenseConstants.SmSeats), + SmServiceAccounts = claimsPrincipal.GetValue(OrganizationLicenseConstants.SmServiceAccounts), + UseRiskInsights = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseRiskInsights), + UseOrganizationDomains = + claimsPrincipal.GetValue(OrganizationLicenseConstants.UseOrganizationDomains), + UseAdminSponsoredFamilies = + claimsPrincipal.GetValue(OrganizationLicenseConstants.UseAdminSponsoredFamilies), + }; + + public static Organization Create( + User owner, + OrganizationLicense license, + string publicKey, + string privateKey) => new() + { + Name = license.Name, + BillingEmail = license.BillingEmail, + BusinessName = license.BusinessName, + PlanType = license.PlanType, + Seats = license.Seats, + MaxCollections = license.MaxCollections, + MaxStorageGb = 10240, + UsePolicies = license.UsePolicies, + UseSso = license.UseSso, + UseKeyConnector = license.UseKeyConnector, + UseScim = license.UseScim, + UseGroups = license.UseGroups, + UseDirectory = license.UseDirectory, + UseEvents = license.UseEvents, + UseTotp = license.UseTotp, + Use2fa = license.Use2fa, + UseApi = license.UseApi, + UseResetPassword = license.UseResetPassword, + Plan = license.Plan, + SelfHost = license.SelfHost, + UsersGetPremium = license.UsersGetPremium, + UseCustomPermissions = license.UseCustomPermissions, + Gateway = null, + GatewayCustomerId = null, + GatewaySubscriptionId = null, + ReferenceData = owner.ReferenceData, + Enabled = license.Enabled, + ExpirationDate = license.Expires, + LicenseKey = license.LicenseKey, + PublicKey = publicKey, + PrivateKey = privateKey, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Status = OrganizationStatusType.Created, + UsePasswordManager = license.UsePasswordManager, + UseSecretsManager = license.UseSecretsManager, + SmSeats = license.SmSeats, + SmServiceAccounts = license.SmServiceAccounts, + UseRiskInsights = license.UseRiskInsights, + UseOrganizationDomains = license.UseOrganizationDomains, + UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies, + }; +} From 57cd628de85494a15a7d2ed1420f292a0d465786 Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Thu, 26 Jun 2025 16:03:05 -0400 Subject: [PATCH 38/47] [PM-17562] Add integration filter support (#5971) * [PM-17562] Add integration filter support * Repond to PR feedback; Remove Date-related filters * Use tables to format the filter class descriptions * [PM-17562] Add database support for integration filters (#5988) * [PM-17562] Add database support for integration filters * Respond to PR review - fix database scripts * Further database updates; fix Filters to be last in views, stored procs, etc * Fix for missing nulls in stored procedures in main migration script * Reorder Filters to the bottom of OrganizationIntegrationConfiguration * Separate out the creation of filters from the IntegrationFilterService to IntegrationFIlterFactory * Move properties to static readonly field * Fix unit tests failing from merge --------- Co-authored-by: Matt Bishop --- ...ionIntegrationConfigurationRequestModel.cs | 30 +- ...onIntegrationConfigurationResponseModel.cs | 2 + .../OrganizationIntegrationConfiguration.cs | 1 + .../IntegrationFilterGroup.cs | 10 + .../IntegrationFilterOperation.cs | 10 + .../IntegrationFilterRule.cs | 11 + ...nizationIntegrationConfigurationDetails.cs | 1 + .../Services/IIntegrationFilterService.cs | 11 + .../EventIntegrationHandler.cs | 61 +- .../IntegrationFilterFactory.cs | 51 + .../IntegrationFilterService.cs | 110 + .../EventIntegrations/README.md | 46 + ...tTypeOrganizationIdIntegrationTypeQuery.cs | 1 + .../Utilities/ServiceCollectionExtensions.cs | 10 +- ...izationIntegrationConfiguration_Create.sql | 9 +- ...izationIntegrationConfiguration_Update.sql | 6 +- .../OrganizationIntegrationConfiguration.sql | 1 + ...ionIntegrationConfigurationDetailsView.sql | 3 +- ...ntegrationsConfigurationControllerTests.cs | 7 + ...tegrationConfigurationRequestModelTests.cs | 69 + .../Services/EventIntegrationHandlerTests.cs | 98 +- .../Services/IntegrationFilterFactoryTests.cs | 46 + .../Services/IntegrationFilterServiceTests.cs | 399 +++ ...ToOrganizationIntegrationConfiguration.sql | 104 + ...zationIntegrationConfiguration.Designer.cs | 3175 ++++++++++++++++ ...sToOrganizationIntegrationConfiguration.cs | 28 + .../DatabaseContextModelSnapshot.cs | 3 + ...zationIntegrationConfiguration.Designer.cs | 3181 +++++++++++++++++ ...sToOrganizationIntegrationConfiguration.cs | 27 + .../DatabaseContextModelSnapshot.cs | 3 + ...zationIntegrationConfiguration.Designer.cs | 3164 ++++++++++++++++ ...sToOrganizationIntegrationConfiguration.cs | 27 + .../DatabaseContextModelSnapshot.cs | 3 + 33 files changed, 10677 insertions(+), 31 deletions(-) create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterGroup.cs create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterOperation.cs create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterRule.cs create mode 100644 src/Core/AdminConsole/Services/IIntegrationFilterService.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterFactory.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterService.cs create mode 100644 test/Core.Test/AdminConsole/Services/IntegrationFilterFactoryTests.cs create mode 100644 test/Core.Test/AdminConsole/Services/IntegrationFilterServiceTests.cs create mode 100644 util/Migrator/DbScripts/2025-06-19_00_AddFiltersToOrganizationIntegrationConfiguration.sql create mode 100644 util/MySqlMigrations/Migrations/20250619185012_AddFiltersToOrganizationIntegrationConfiguration.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250619185012_AddFiltersToOrganizationIntegrationConfiguration.cs create mode 100644 util/PostgresMigrations/Migrations/20250619184947_AddFiltersToOrganizationIntegrationConfiguration.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250619184947_AddFiltersToOrganizationIntegrationConfiguration.cs create mode 100644 util/SqliteMigrations/Migrations/20250619184959_AddFiltersToOrganizationIntegrationConfiguration.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250619184959_AddFiltersToOrganizationIntegrationConfiguration.cs diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs index 85d46c791b..d4d69f77c1 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs @@ -15,6 +15,8 @@ public class OrganizationIntegrationConfigurationRequestModel [Required] public EventType EventType { get; set; } + public string? Filters { get; set; } + public string? Template { get; set; } public bool IsValidForType(IntegrationType integrationType) @@ -24,9 +26,13 @@ public class OrganizationIntegrationConfigurationRequestModel case IntegrationType.CloudBillingSync or IntegrationType.Scim: return false; case IntegrationType.Slack: - return !string.IsNullOrWhiteSpace(Template) && IsConfigurationValid(); + return !string.IsNullOrWhiteSpace(Template) && + IsConfigurationValid() && + IsFiltersValid(); case IntegrationType.Webhook: - return !string.IsNullOrWhiteSpace(Template) && IsConfigurationValid(); + return !string.IsNullOrWhiteSpace(Template) && + IsConfigurationValid() && + IsFiltersValid(); default: return false; @@ -39,6 +45,7 @@ public class OrganizationIntegrationConfigurationRequestModel { OrganizationIntegrationId = organizationIntegrationId, Configuration = Configuration, + Filters = Filters, EventType = EventType, Template = Template }; @@ -48,6 +55,7 @@ public class OrganizationIntegrationConfigurationRequestModel { currentConfiguration.Configuration = Configuration; currentConfiguration.EventType = EventType; + currentConfiguration.Filters = Filters; currentConfiguration.Template = Template; return currentConfiguration; @@ -70,4 +78,22 @@ public class OrganizationIntegrationConfigurationRequestModel return false; } } + + private bool IsFiltersValid() + { + if (Filters is null) + { + return true; + } + + try + { + var filters = JsonSerializer.Deserialize(Filters); + return filters is not null; + } + catch + { + return false; + } + } } diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs index 8d074509c5..590a32ee3d 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs @@ -17,11 +17,13 @@ public class OrganizationIntegrationConfigurationResponseModel : ResponseModel Configuration = organizationIntegrationConfiguration.Configuration; CreationDate = organizationIntegrationConfiguration.CreationDate; EventType = organizationIntegrationConfiguration.EventType; + Filters = organizationIntegrationConfiguration.Filters; Template = organizationIntegrationConfiguration.Template; } public Guid Id { get; set; } public string? Configuration { get; set; } + public string? Filters { get; set; } public DateTime CreationDate { 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 25b669622f..5c2250824e 100644 --- a/src/Core/AdminConsole/Entities/OrganizationIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Entities/OrganizationIntegrationConfiguration.cs @@ -15,5 +15,6 @@ public class OrganizationIntegrationConfiguration : ITableObject public string? Template { get; set; } public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; set; } = DateTime.UtcNow; + public string? Filters { get; set; } public void SetNewId() => Id = CoreHelpers.GenerateComb(); } diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterGroup.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterGroup.cs new file mode 100644 index 0000000000..bb0c2e01ba --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterGroup.cs @@ -0,0 +1,10 @@ +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public class IntegrationFilterGroup +{ + public bool AndOperator { get; init; } = true; + public List? Rules { get; init; } + public List? Groups { get; init; } +} diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterOperation.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterOperation.cs new file mode 100644 index 0000000000..f09df47738 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterOperation.cs @@ -0,0 +1,10 @@ +#nullable enable +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public enum IntegrationFilterOperation +{ + Equals = 0, + NotEquals = 1, + In = 2, + NotIn = 3 +} diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterRule.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterRule.cs new file mode 100644 index 0000000000..b9d90a0442 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterRule.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public class IntegrationFilterRule +{ + public required string Property { get; set; } + public required IntegrationFilterOperation Operation { get; set; } + public required object? Value { get; set; } +} + diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs index 139a7aff25..b5d224c012 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs @@ -12,6 +12,7 @@ public class OrganizationIntegrationConfigurationDetails public IntegrationType IntegrationType { get; set; } public EventType EventType { get; set; } public string? Configuration { get; set; } + public string? Filters { get; set; } public string? IntegrationConfiguration { get; set; } public string? Template { get; set; } diff --git a/src/Core/AdminConsole/Services/IIntegrationFilterService.cs b/src/Core/AdminConsole/Services/IIntegrationFilterService.cs new file mode 100644 index 0000000000..5bc035d468 --- /dev/null +++ b/src/Core/AdminConsole/Services/IIntegrationFilterService.cs @@ -0,0 +1,11 @@ +#nullable enable + +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Models.Data; + +namespace Bit.Core.Services; + +public interface IIntegrationFilterService +{ + bool EvaluateFilterGroup(IntegrationFilterGroup group, EventMessage message); +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs index e728d7c5c0..3ffd08edd2 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs @@ -6,15 +6,18 @@ using Bit.Core.AdminConsole.Utilities; using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Repositories; +using Microsoft.Extensions.Logging; namespace Bit.Core.Services; public class EventIntegrationHandler( IntegrationType integrationType, IEventIntegrationPublisher eventIntegrationPublisher, + IIntegrationFilterService integrationFilterService, IOrganizationIntegrationConfigurationRepository configurationRepository, IUserRepository userRepository, - IOrganizationRepository organizationRepository) + IOrganizationRepository organizationRepository, + ILogger> logger) : IEventMessageHandler { public async Task HandleEventAsync(EventMessage eventMessage) @@ -31,25 +34,47 @@ public class EventIntegrationHandler( foreach (var configuration in configurations) { - var template = configuration.Template ?? string.Empty; - var context = await BuildContextAsync(eventMessage, template); - var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(template, context); - var messageId = eventMessage.IdempotencyId ?? Guid.NewGuid(); - - var config = configuration.MergedConfiguration.Deserialize() - ?? throw new InvalidOperationException($"Failed to deserialize to {typeof(T).Name}"); - - var message = new IntegrationMessage + try { - IntegrationType = integrationType, - MessageId = messageId.ToString(), - Configuration = config, - RenderedTemplate = renderedTemplate, - RetryCount = 0, - DelayUntilDate = null - }; + if (configuration.Filters is string filterJson) + { + // Evaluate filters - if false, then discard and do not process + var filters = JsonSerializer.Deserialize(filterJson) + ?? throw new InvalidOperationException($"Failed to deserialize Filters to FilterGroup"); + if (!integrationFilterService.EvaluateFilterGroup(filters, eventMessage)) + { + continue; + } + } - await eventIntegrationPublisher.PublishAsync(message); + // Valid filter - assemble message and publish to Integration topic/exchange + var template = configuration.Template ?? string.Empty; + var context = await BuildContextAsync(eventMessage, template); + var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(template, context); + var messageId = eventMessage.IdempotencyId ?? Guid.NewGuid(); + var config = configuration.MergedConfiguration.Deserialize() + ?? throw new InvalidOperationException($"Failed to deserialize to {typeof(T).Name} - bad Configuration"); + + var message = new IntegrationMessage + { + IntegrationType = integrationType, + MessageId = messageId.ToString(), + Configuration = config, + RenderedTemplate = renderedTemplate, + RetryCount = 0, + DelayUntilDate = null + }; + + await eventIntegrationPublisher.PublishAsync(message); + } + catch (Exception exception) + { + logger.LogError( + exception, + "Failed to publish Integration Message for {Type}, check Id {RecordId} for error in Configuration or Filters", + typeof(T).Name, + configuration.Id); + } } } diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterFactory.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterFactory.cs new file mode 100644 index 0000000000..b90ea8d16e --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterFactory.cs @@ -0,0 +1,51 @@ +#nullable enable + +using System.Linq.Expressions; +using Bit.Core.Models.Data; + +namespace Bit.Core.Services; + +public delegate bool IntegrationFilter(EventMessage message, object? value); + +public static class IntegrationFilterFactory +{ + public static IntegrationFilter BuildEqualityFilter(string propertyName) + { + var param = Expression.Parameter(typeof(EventMessage), "m"); + var valueParam = Expression.Parameter(typeof(object), "val"); + + var property = Expression.PropertyOrField(param, propertyName); + var typedVal = Expression.Convert(valueParam, typeof(T)); + var body = Expression.Equal(property, typedVal); + + var lambda = Expression.Lambda>(body, param, valueParam); + return new IntegrationFilter(lambda.Compile()); + } + + public static IntegrationFilter BuildInFilter(string propertyName) + { + var param = Expression.Parameter(typeof(EventMessage), "m"); + var valueParam = Expression.Parameter(typeof(object), "val"); + + var property = Expression.PropertyOrField(param, propertyName); + + var method = typeof(Enumerable) + .GetMethods() + .FirstOrDefault(m => + m.Name == "Contains" + && m.GetParameters().Length == 2) + ?.MakeGenericMethod(typeof(T)); + if (method is null) + { + throw new InvalidOperationException("Could not find Contains method."); + } + + var listType = typeof(IEnumerable); + var castedList = Expression.Convert(valueParam, listType); + + var containsCall = Expression.Call(method, castedList, property); + + var lambda = Expression.Lambda>(containsCall, param, valueParam); + return new IntegrationFilter(lambda.Compile()); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterService.cs new file mode 100644 index 0000000000..88877c329a --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterService.cs @@ -0,0 +1,110 @@ +#nullable enable + +using System.Text.Json; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Models.Data; + +namespace Bit.Core.Services; + +public class IntegrationFilterService : IIntegrationFilterService +{ + private readonly Dictionary _equalsFilters = new(); + private readonly Dictionary _inFilters = new(); + private static readonly string[] _filterableProperties = new[] + { + "UserId", + "InstallationId", + "ProviderId", + "CipherId", + "CollectionId", + "GroupId", + "PolicyId", + "OrganizationUserId", + "ProviderUserId", + "ProviderOrganizationId", + "ActingUserId", + "SecretId", + "ServiceAccountId" + }; + + public IntegrationFilterService() + { + BuildFilters(); + } + + public bool EvaluateFilterGroup(IntegrationFilterGroup group, EventMessage message) + { + var ruleResults = group.Rules?.Select( + rule => EvaluateRule(rule, message) + ) ?? Enumerable.Empty(); + var groupResults = group.Groups?.Select( + innerGroup => EvaluateFilterGroup(innerGroup, message) + ) ?? Enumerable.Empty(); + + var results = ruleResults.Concat(groupResults); + return group.AndOperator ? results.All(r => r) : results.Any(r => r); + } + + private bool EvaluateRule(IntegrationFilterRule rule, EventMessage message) + { + var key = rule.Property; + return rule.Operation switch + { + IntegrationFilterOperation.Equals => _equalsFilters.TryGetValue(key, out var equals) && + equals(message, ToGuid(rule.Value)), + IntegrationFilterOperation.NotEquals => !(_equalsFilters.TryGetValue(key, out var equals) && + equals(message, ToGuid(rule.Value))), + IntegrationFilterOperation.In => _inFilters.TryGetValue(key, out var inList) && + inList(message, ToGuidList(rule.Value)), + IntegrationFilterOperation.NotIn => !(_inFilters.TryGetValue(key, out var inList) && + inList(message, ToGuidList(rule.Value))), + _ => false + }; + } + + private void BuildFilters() + { + foreach (var property in _filterableProperties) + { + _equalsFilters[property] = IntegrationFilterFactory.BuildEqualityFilter(property); + _inFilters[property] = IntegrationFilterFactory.BuildInFilter(property); + } + } + + private static Guid? ToGuid(object? value) + { + if (value is Guid guid) + { + return guid; + } + if (value is string stringValue) + { + return Guid.Parse(stringValue); + } + if (value is JsonElement jsonElement) + { + return jsonElement.GetGuid(); + } + + throw new InvalidCastException("Could not convert value to Guid"); + } + + private static IEnumerable ToGuidList(object? value) + { + if (value is IEnumerable guidList) + { + return guidList; + } + if (value is JsonElement { ValueKind: JsonValueKind.Array } jsonElement) + { + var list = new List(); + foreach (var item in jsonElement.EnumerateArray()) + { + list.Add(ToGuid(item)); + } + return list; + } + + throw new InvalidCastException("Could not convert value to Guid[]"); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md index 54b36d2595..7320f8bab7 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md @@ -228,6 +228,52 @@ Currently, there are integrations / handlers for Slack and webhooks (as mentione - An array of `OrganizationIntegrationConfigurationDetails` is what the `EventIntegrationHandler` fetches from the database to determine what to publish at the integration level. +## Filtering + +In addition to the ability to configure integrations mentioned above, organization admins can +also add `Filters` stored in the `OrganizationIntegrationConfiguration`. Filters are completely +optional and as simple or complex as organization admins want to make them. These are stored in +the database as JSON and serialized into an `IntegrationFilterGroup`. This is then passed to +the `IntegrationFilterService`, which evaluates it to a `bool`. If it's `true`, the integration +proceeds as above. If it's `false`, we ignore this event and do not route it to the integration +level. + +### `IntegrationFilterGroup` + +Logical AND / OR grouping of a number of rules and other subgroups. + +| Property | Description | +|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `AndOperator` | Indicates whether **all** (`true`) or **any** (`false`) of the `Rules` and `Groups` must be true. This applies to _both_ the inner group and the list of rules; for instance, if this group contained Rule1 and Rule2 as well as Group1 and Group2:

`true`: `Rule1 && Rule2 && Group1 && Group2`
`false`: `Rule1 \|\| Rule2 \|\| Group1 \|\| Group2` | +| `Rules` | A list of `IntegrationFilterRule`. Can be null or empty, in which case it will return `true`. | +| `Groups` | A list of nested `IntegrationFilterGroup`. Can be null or empty, in which case it will return `true`. | + +### `IntegrationFilterRule` + +The core of the filtering framework to determine if the data in this specific EventMessage +matches the data for which the filter is searching. + +| Property | Description | +|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Property` | The property on `EventMessage` to evaluate (e.g., `CollectionId`). | +| `Operation` | The comparison to perform between the property and `Value`.

**Supported operations:**
• `Equals`: `Guid` equals `Value`
• `NotEquals`: logical inverse of `Equals`
• `In`: `Guid` is in `Value` list
• `NotIn`: logical inverse of `In` | +| `Value` | The comparison value. Type depends on `Operation`:
• `Equals`, `NotEquals`: `Guid`
• `In`, `NotIn`: list of `Guid` | + +```mermaid +graph TD + A[IntegrationFilterGroup] + A -->|Has 0..many| B1[IntegrationFilterRule] + A --> D1[And Operator] + A -->|Has 0..many| C1[Nested IntegrationFilterGroup] + + B1 --> B2[Property: string] + B1 --> B3[Operation: Equals/In/DateBefore/DateAfter] + B1 --> B4[Value: object?] + + C1 -->|Has many| B1_2[IntegrationFilterRule] + C1 -->|Can contain| C2[IntegrationFilterGroup...] +``` + # Building a new integration These are all the pieces required in the process of building out a new integration. For diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs index 1a54d6588a..c816b01a01 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs @@ -31,6 +31,7 @@ public class OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrgan IntegrationType = oi.Type, EventType = oic.EventType, Configuration = oic.Configuration, + Filters = oic.Filters, IntegrationConfiguration = oi.Configuration, Template = oic.Template }; diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 83015354bb..1c4473674c 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -614,9 +614,11 @@ public static class ServiceCollectionExtensions new EventIntegrationHandler( integrationType, provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), - provider.GetRequiredService())); + provider.GetRequiredService(), + provider.GetRequiredService>>())); services.AddSingleton(provider => new AzureServiceBusEventListenerService( @@ -647,6 +649,7 @@ public static class ServiceCollectionExtensions !CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName)) return services; + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddAzureServiceBusEventRepositoryListener(globalSettings); @@ -697,9 +700,11 @@ public static class ServiceCollectionExtensions new EventIntegrationHandler( integrationType, provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), - provider.GetRequiredService())); + provider.GetRequiredService(), + provider.GetRequiredService>>())); services.AddSingleton(provider => new RabbitMqEventListenerService( @@ -730,6 +735,7 @@ public static class ServiceCollectionExtensions return services; } + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddRabbitMqEventRepositoryListener(globalSettings); diff --git a/src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_Create.sql b/src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_Create.sql index 18160c9ea9..ab40ea5d4f 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_Create.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_Create.sql @@ -5,7 +5,8 @@ @Configuration VARCHAR(MAX), @Template VARCHAR(MAX), @CreationDate DATETIME2(7), - @RevisionDate DATETIME2(7) + @RevisionDate DATETIME2(7), + @Filters VARCHAR(MAX) = NULL AS BEGIN SET NOCOUNT ON @@ -18,7 +19,8 @@ BEGIN [Configuration], [Template], [CreationDate], - [RevisionDate] + [RevisionDate], + [Filters] ) VALUES ( @@ -28,6 +30,7 @@ BEGIN @Configuration, @Template, @CreationDate, - @RevisionDate + @RevisionDate, + @Filters ) END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_Update.sql b/src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_Update.sql index b613774d04..903844ab70 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_Update.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_Update.sql @@ -5,7 +5,8 @@ @Configuration VARCHAR(MAX), @Template VARCHAR(MAX), @CreationDate DATETIME2(7), - @RevisionDate DATETIME2(7) + @RevisionDate DATETIME2(7), + @Filters VARCHAR(MAX) = NULL AS BEGIN SET NOCOUNT ON @@ -18,7 +19,8 @@ BEGIN [Configuration] = @Configuration, [Template] = @Template, [CreationDate] = @CreationDate, - [RevisionDate] = @RevisionDate + [RevisionDate] = @RevisionDate, + [Filters] = @Filters WHERE [Id] = @Id END diff --git a/src/Sql/dbo/Tables/OrganizationIntegrationConfiguration.sql b/src/Sql/dbo/Tables/OrganizationIntegrationConfiguration.sql index 28283f8015..b46ca81141 100644 --- a/src/Sql/dbo/Tables/OrganizationIntegrationConfiguration.sql +++ b/src/Sql/dbo/Tables/OrganizationIntegrationConfiguration.sql @@ -7,6 +7,7 @@ CREATE TABLE [dbo].[OrganizationIntegrationConfiguration] [Template] VARCHAR (MAX) NULL, [CreationDate] DATETIME2 (7) NOT NULL, [RevisionDate] DATETIME2 (7) NOT NULL, + [Filters] VARCHAR (MAX) NULL, CONSTRAINT [PK_OrganizationIntegrationConfiguration] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_OrganizationIntegrationConfiguration_OrganizationIntegration] FOREIGN KEY ([OrganizationIntegrationId]) REFERENCES [dbo].[OrganizationIntegration] ([Id]) ON DELETE CASCADE ); diff --git a/src/Sql/dbo/Views/OrganizationIntegrationConfigurationDetailsView.sql b/src/Sql/dbo/Views/OrganizationIntegrationConfigurationDetailsView.sql index 45609da551..e66c036250 100644 --- a/src/Sql/dbo/Views/OrganizationIntegrationConfigurationDetailsView.sql +++ b/src/Sql/dbo/Views/OrganizationIntegrationConfigurationDetailsView.sql @@ -6,7 +6,8 @@ AS oic.[EventType], oic.[Configuration], oi.[Configuration] AS [IntegrationConfiguration], - oic.[Template] + oic.[Template], + oic.[Filters] FROM [dbo].[OrganizationIntegrationConfiguration] oic INNER JOIN diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs index 77283475cf..4732ddd748 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs @@ -154,6 +154,7 @@ public class OrganizationIntegrationsConfigurationControllerTests var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456"); model.Configuration = JsonSerializer.Serialize(slackConfig); model.Template = "Template String"; + model.Filters = null; var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration); @@ -191,6 +192,7 @@ public class OrganizationIntegrationsConfigurationControllerTests var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN"); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = "Template String"; + model.Filters = null; var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration); @@ -228,6 +230,7 @@ public class OrganizationIntegrationsConfigurationControllerTests var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost"); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = "Template String"; + model.Filters = null; var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration); @@ -433,6 +436,7 @@ public class OrganizationIntegrationsConfigurationControllerTests var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456"); model.Configuration = JsonSerializer.Serialize(slackConfig); model.Template = "Template String"; + model.Filters = null; var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration)); @@ -476,6 +480,7 @@ public class OrganizationIntegrationsConfigurationControllerTests var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN"); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = "Template String"; + model.Filters = null; var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration)); @@ -518,6 +523,7 @@ public class OrganizationIntegrationsConfigurationControllerTests var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost"); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = "Template String"; + model.Filters = null; var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration)); @@ -558,6 +564,7 @@ public class OrganizationIntegrationsConfigurationControllerTests var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN"); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = "Template String"; + model.Filters = null; sutProvider.Sut.Url = Substitute.For(); sutProvider.GetDependency() diff --git a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs index 5958aa06aa..20831ec7d9 100644 --- a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs +++ b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs @@ -65,6 +65,21 @@ public class OrganizationIntegrationConfigurationRequestModelTests Assert.False(model.IsValidForType(IntegrationType.Webhook)); } + + [Fact] + public void IsValidForType_InvalidJsonFilters_ReturnsFalse() + { + var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com")); + var model = new OrganizationIntegrationConfigurationRequestModel + { + Configuration = config, + Filters = "{Not valid json", + Template = "template" + }; + + Assert.False(model.IsValidForType(IntegrationType.Webhook)); + } + [Fact] public void IsValidForType_ScimIntegration_ReturnsFalse() { @@ -91,6 +106,33 @@ public class OrganizationIntegrationConfigurationRequestModelTests Assert.True(model.IsValidForType(IntegrationType.Slack)); } + [Fact] + public void IsValidForType_ValidSlackConfigurationWithFilters_ReturnsTrue() + { + var config = JsonSerializer.Serialize(new SlackIntegrationConfiguration("C12345")); + var filters = JsonSerializer.Serialize(new IntegrationFilterGroup() + { + AndOperator = true, + Rules = [ + new IntegrationFilterRule() + { + Operation = IntegrationFilterOperation.Equals, + Property = "CollectionId", + Value = Guid.NewGuid() + } + ], + Groups = [] + }); + var model = new OrganizationIntegrationConfigurationRequestModel + { + Configuration = config, + Filters = filters, + Template = "template" + }; + + Assert.True(model.IsValidForType(IntegrationType.Slack)); + } + [Fact] public void IsValidForType_ValidNoAuthWebhookConfiguration_ReturnsTrue() { @@ -117,6 +159,33 @@ public class OrganizationIntegrationConfigurationRequestModelTests Assert.True(model.IsValidForType(IntegrationType.Webhook)); } + [Fact] + public void IsValidForType_ValidWebhookConfigurationWithFilters_ReturnsTrue() + { + var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN")); + var filters = JsonSerializer.Serialize(new IntegrationFilterGroup() + { + AndOperator = true, + Rules = [ + new IntegrationFilterRule() + { + Operation = IntegrationFilterOperation.Equals, + Property = "CollectionId", + Value = Guid.NewGuid() + } + ], + Groups = [] + }); + var model = new OrganizationIntegrationConfigurationRequestModel + { + Configuration = config, + Filters = filters, + Template = "template" + }; + + Assert.True(model.IsValidForType(IntegrationType.Webhook)); + } + [Fact] public void IsValidForType_UnknownIntegrationType_ReturnsFalse() { diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs index 8dcc49a8f1..e4ffabf691 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs @@ -10,6 +10,7 @@ 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 NSubstitute; using Xunit; @@ -25,6 +26,8 @@ public class EventIntegrationHandlerTests private const string _url = "https://localhost"; private const string _url2 = "https://example.com"; private readonly IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For(); + private readonly ILogger> _logger = + Substitute.For>>(); private SutProvider> GetSutProvider( List configurations) @@ -37,6 +40,7 @@ public class EventIntegrationHandlerTests .SetDependency(configurationRepository) .SetDependency(_eventIntegrationPublisher) .SetDependency(IntegrationType.Webhook) + .SetDependency(_logger) .Create(); } @@ -82,6 +86,29 @@ public class EventIntegrationHandlerTests return [config, config2]; } + private static List InvalidFilterConfiguration() + { + var config = Substitute.For(); + config.Configuration = null; + config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url }); + config.Template = _templateBase; + config.Filters = "Invalid Configuration!"; + + return [config]; + } + + private static List ValidFilterConfiguration() + { + var config = Substitute.For(); + config.Configuration = null; + config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url }); + config.Template = _templateBase; + config.Filters = JsonSerializer.Serialize(new IntegrationFilterGroup() { }); + + return [config]; + } + + [Theory, BitAutoData] public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage) { @@ -92,7 +119,7 @@ public class EventIntegrationHandlerTests } [Theory, BitAutoData] - public async Task HandleEventAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(EventMessage eventMessage) + public async Task HandleEventAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessage(EventMessage eventMessage) { var sutProvider = GetSutProvider(OneConfiguration(_templateBase)); @@ -109,6 +136,27 @@ public class EventIntegrationHandlerTests await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); } + [Theory, BitAutoData] + public async Task HandleEventAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(TwoConfigurations(_templateBase)); + + await sutProvider.Sut.HandleEventAsync(eventMessage); + + var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" + ); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + + expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_url2); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + } + [Theory, BitAutoData] public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage) { @@ -170,6 +218,50 @@ public class EventIntegrationHandlerTests await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty); } + [Theory, BitAutoData] + public async Task HandleEventAsync_FilterReturnsFalse_DoesNothing(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(ValidFilterConfiguration()); + sutProvider.GetDependency().EvaluateFilterGroup( + Arg.Any(), Arg.Any()).Returns(false); + + await sutProvider.Sut.HandleEventAsync(eventMessage); + Assert.Empty(_eventIntegrationPublisher.ReceivedCalls()); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_FilterReturnsTrue_PublishesIntegrationMessage(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(ValidFilterConfiguration()); + sutProvider.GetDependency().EvaluateFilterGroup( + Arg.Any(), Arg.Any()).Returns(true); + + await sutProvider.Sut.HandleEventAsync(eventMessage); + + var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" + ); + + Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_InvalidFilter_LogsErrorDoesNothing(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(InvalidFilterConfiguration()); + + await sutProvider.Sut.HandleEventAsync(eventMessage); + Assert.Empty(_eventIntegrationPublisher.ReceivedCalls()); + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + [Theory, BitAutoData] public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List eventMessages) { @@ -180,7 +272,7 @@ public class EventIntegrationHandlerTests } [Theory, BitAutoData] - public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(List eventMessages) + public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessages(List eventMessages) { var sutProvider = GetSutProvider(OneConfiguration(_templateBase)); @@ -197,7 +289,7 @@ public class EventIntegrationHandlerTests } [Theory, BitAutoData] - public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_CallsProcessEventIntegrationAsyncMultipleTimes( + public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages( List eventMessages) { var sutProvider = GetSutProvider(TwoConfigurations(_templateBase)); diff --git a/test/Core.Test/AdminConsole/Services/IntegrationFilterFactoryTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationFilterFactoryTests.cs new file mode 100644 index 0000000000..b408bc1501 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/IntegrationFilterFactoryTests.cs @@ -0,0 +1,46 @@ +using Bit.Core.Models.Data; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.Services; + +public class IntegrationFilterFactoryTests +{ + [Theory, BitAutoData] + public void BuildEqualityFilter_ReturnsCorrectMatch(EventMessage message) + { + var different = Guid.NewGuid(); + var expected = Guid.NewGuid(); + message.UserId = expected; + + var filter = IntegrationFilterFactory.BuildEqualityFilter("UserId"); + + Assert.True(filter(message, expected)); + Assert.False(filter(message, different)); + } + + [Theory, BitAutoData] + public void BuildEqualityFilter_UserIdIsNull_ReturnsFalse(EventMessage message) + { + message.UserId = null; + + var filter = IntegrationFilterFactory.BuildEqualityFilter("UserId"); + + Assert.False(filter(message, Guid.NewGuid())); + } + + [Theory, BitAutoData] + public void BuildInFilter_ReturnsCorrectMatch(EventMessage message) + { + var match = Guid.NewGuid(); + message.UserId = match; + var inList = new List { Guid.NewGuid(), match, Guid.NewGuid() }; + var outList = new List { Guid.NewGuid(), Guid.NewGuid() }; + + var filter = IntegrationFilterFactory.BuildInFilter("UserId"); + + Assert.True(filter(message, inList)); + Assert.False(filter(message, outList)); + } +} diff --git a/test/Core.Test/AdminConsole/Services/IntegrationFilterServiceTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationFilterServiceTests.cs new file mode 100644 index 0000000000..4143469a4b --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/IntegrationFilterServiceTests.cs @@ -0,0 +1,399 @@ +#nullable enable + +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.Attributes; +using Xunit; + +namespace Bit.Core.Test.Services; + +public class IntegrationFilterServiceTests +{ + private readonly IntegrationFilterService _service = new(); + + [Theory, BitAutoData] + public void EvaluateFilterGroup_EqualsUserId_Matches(EventMessage eventMessage) + { + var userId = Guid.NewGuid(); + eventMessage.UserId = userId; + + var group = new IntegrationFilterGroup + { + AndOperator = true, + Rules = + [ + new() + { + Property = "UserId", + Operation = IntegrationFilterOperation.Equals, + Value = userId + } + ] + }; + + var result = _service.EvaluateFilterGroup(group, eventMessage); + Assert.True(result); + + var jsonGroup = JsonSerializer.Serialize(group); + var roundtrippedGroup = JsonSerializer.Deserialize(jsonGroup); + Assert.NotNull(roundtrippedGroup); + Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage)); + } + + [Theory, BitAutoData] + public void EvaluateFilterGroup_EqualsUserId_DoesNotMatch(EventMessage eventMessage) + { + eventMessage.UserId = Guid.NewGuid(); + var otherUserId = Guid.NewGuid(); + + var group = new IntegrationFilterGroup + { + AndOperator = true, + Rules = + [ + new() + { + Property = "UserId", + Operation = IntegrationFilterOperation.Equals, + Value = otherUserId + } + ] + }; + + var result = _service.EvaluateFilterGroup(group, eventMessage); + Assert.False(result); + + var jsonGroup = JsonSerializer.Serialize(group); + var roundtrippedGroup = JsonSerializer.Deserialize(jsonGroup); + Assert.NotNull(roundtrippedGroup); + Assert.False(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage)); + } + + [Theory, BitAutoData] + public void EvaluateFilterGroup_NotEqualsUniqueUserId_ReturnsTrue(EventMessage eventMessage) + { + var otherId = Guid.NewGuid(); + eventMessage.UserId = otherId; + + var group = new IntegrationFilterGroup + { + AndOperator = true, + Rules = + [ + new() + { + Property = "UserId", + Operation = IntegrationFilterOperation.NotEquals, + Value = Guid.NewGuid() + } + ] + }; + + var result = _service.EvaluateFilterGroup(group, eventMessage); + Assert.True(result); + + var jsonGroup = JsonSerializer.Serialize(group); + var roundtrippedGroup = JsonSerializer.Deserialize(jsonGroup); + Assert.NotNull(roundtrippedGroup); + Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage)); + } + + [Theory, BitAutoData] + public void EvaluateFilterGroup_NotEqualsMatchingUserId_ReturnsFalse(EventMessage eventMessage) + { + var id = Guid.NewGuid(); + eventMessage.UserId = id; + + var group = new IntegrationFilterGroup + { + AndOperator = true, + Rules = + [ + new() + { + Property = "UserId", + Operation = IntegrationFilterOperation.NotEquals, + Value = id + } + ] + }; + + var result = _service.EvaluateFilterGroup(group, eventMessage); + Assert.False(result); + + var jsonGroup = JsonSerializer.Serialize(group); + var roundtrippedGroup = JsonSerializer.Deserialize(jsonGroup); + Assert.NotNull(roundtrippedGroup); + Assert.False(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage)); + } + + [Theory, BitAutoData] + public void EvaluateFilterGroup_InCollectionId_Matches(EventMessage eventMessage) + { + var id = Guid.NewGuid(); + eventMessage.CollectionId = id; + + var group = new IntegrationFilterGroup + { + AndOperator = true, + Rules = + [ + new() + { + Property = "CollectionId", + Operation = IntegrationFilterOperation.In, + Value = new Guid?[] { Guid.NewGuid(), id } + } + ] + }; + + var result = _service.EvaluateFilterGroup(group, eventMessage); + Assert.True(result); + + var jsonGroup = JsonSerializer.Serialize(group); + var roundtrippedGroup = JsonSerializer.Deserialize(jsonGroup); + Assert.NotNull(roundtrippedGroup); + Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage)); + } + + [Theory, BitAutoData] + public void EvaluateFilterGroup_InCollectionId_DoesNotMatch(EventMessage eventMessage) + { + eventMessage.CollectionId = Guid.NewGuid(); + + var group = new IntegrationFilterGroup + { + AndOperator = true, + Rules = + [ + new() + { + Property = "CollectionId", + Operation = IntegrationFilterOperation.In, + Value = new Guid?[] { Guid.NewGuid(), Guid.NewGuid() } + } + ] + }; + + var result = _service.EvaluateFilterGroup(group, eventMessage); + Assert.False(result); + + var jsonGroup = JsonSerializer.Serialize(group); + var roundtrippedGroup = JsonSerializer.Deserialize(jsonGroup); + Assert.NotNull(roundtrippedGroup); + Assert.False(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage)); + } + + [Theory, BitAutoData] + public void EvaluateFilterGroup_NotInCollectionIdUniqueId_ReturnsTrue(EventMessage eventMessage) + { + eventMessage.CollectionId = Guid.NewGuid(); + + var group = new IntegrationFilterGroup + { + AndOperator = true, + Rules = + [ + new() + { + Property = "CollectionId", + Operation = IntegrationFilterOperation.NotIn, + Value = new Guid?[] { Guid.NewGuid(), Guid.NewGuid() } + } + ] + }; + + var result = _service.EvaluateFilterGroup(group, eventMessage); + Assert.True(result); + + var jsonGroup = JsonSerializer.Serialize(group); + var roundtrippedGroup = JsonSerializer.Deserialize(jsonGroup); + Assert.NotNull(roundtrippedGroup); + Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage)); + } + + [Theory, BitAutoData] + public void EvaluateFilterGroup_NotInCollectionIdPresent_ReturnsFalse(EventMessage eventMessage) + { + var matchId = Guid.NewGuid(); + eventMessage.CollectionId = matchId; + + var group = new IntegrationFilterGroup + { + AndOperator = true, + Rules = + [ + new() + { + Property = "CollectionId", + Operation = IntegrationFilterOperation.NotIn, + Value = new Guid?[] { Guid.NewGuid(), matchId } + } + ] + }; + + var result = _service.EvaluateFilterGroup(group, eventMessage); + Assert.False(result); + + var jsonGroup = JsonSerializer.Serialize(group); + var roundtrippedGroup = JsonSerializer.Deserialize(jsonGroup); + Assert.NotNull(roundtrippedGroup); + Assert.False(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage)); + } + + [Theory, BitAutoData] + public void EvaluateFilterGroup_NestedGroups_AllMatch(EventMessage eventMessage) + { + var id = Guid.NewGuid(); + var collectionId = Guid.NewGuid(); + eventMessage.UserId = id; + eventMessage.CollectionId = collectionId; + + var nestedGroup = new IntegrationFilterGroup + { + AndOperator = true, + Rules = + [ + new() { Property = "UserId", Operation = IntegrationFilterOperation.Equals, Value = id }, + new() + { + Property = "CollectionId", + Operation = IntegrationFilterOperation.In, + Value = new Guid?[] { collectionId, Guid.NewGuid() } + } + ] + }; + + var topGroup = new IntegrationFilterGroup + { + AndOperator = true, + Groups = [nestedGroup] + }; + + var result = _service.EvaluateFilterGroup(topGroup, eventMessage); + Assert.True(result); + + var jsonGroup = JsonSerializer.Serialize(topGroup); + var roundtrippedGroup = JsonSerializer.Deserialize(jsonGroup); + Assert.NotNull(roundtrippedGroup); + Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage)); + } + + [Theory, BitAutoData] + public void EvaluateFilterGroup_UnknownProperty_ReturnsFalse(EventMessage eventMessage) + { + var group = new IntegrationFilterGroup + { + Rules = + [ + new() { Property = "NotARealProperty", Operation = IntegrationFilterOperation.Equals, Value = "test" } + ] + }; + + var result = _service.EvaluateFilterGroup(group, eventMessage); + Assert.False(result); + } + + [Theory, BitAutoData] + public void EvaluateFilterGroup_UnsupportedOperation_ReturnsFalse(EventMessage eventMessage) + { + var group = new IntegrationFilterGroup + { + Rules = + [ + new() + { + Property = "UserId", + Operation = (IntegrationFilterOperation)999, // Unknown operation + Value = eventMessage.UserId + } + ] + }; + + var result = _service.EvaluateFilterGroup(group, eventMessage); + Assert.False(result); + } + + [Theory, BitAutoData] + public void EvaluateFilterGroup_WrongTypeForInList_ThrowsException(EventMessage eventMessage) + { + var group = new IntegrationFilterGroup + { + Rules = + [ + new() + { + Property = "CollectionId", + Operation = IntegrationFilterOperation.In, + Value = "not an array" // Should be Guid[] + } + ] + }; + + Assert.Throws(() => + _service.EvaluateFilterGroup(group, eventMessage)); + } + + [Theory, BitAutoData] + public void EvaluateFilterGroup_NullValue_ThrowsException(EventMessage eventMessage) + { + var group = new IntegrationFilterGroup + { + Rules = + [ + new() + { + Property = "UserId", + Operation = IntegrationFilterOperation.Equals, + Value = null + } + ] + }; + + Assert.Throws(() => + _service.EvaluateFilterGroup(group, eventMessage)); + } + + [Theory, BitAutoData] + public void EvaluateFilterGroup_EmptyRuleList_ReturnsTrue(EventMessage eventMessage) + { + var group = new IntegrationFilterGroup + { + Rules = [], + Groups = [], + AndOperator = true + }; + + var result = _service.EvaluateFilterGroup(group, eventMessage); + Assert.True(result); // Nothing to fail, returns true by design + } + + [Theory, BitAutoData] + public void EvaluateFilterGroup_InvalidNestedGroup_ReturnsFalse(EventMessage eventMessage) + { + var group = new IntegrationFilterGroup + { + Groups = + [ + new() + { + Rules = + [ + new() + { + Property = "Nope", + Operation = IntegrationFilterOperation.Equals, + Value = "bad" + } + ] + } + ], + AndOperator = true + }; + + var result = _service.EvaluateFilterGroup(group, eventMessage); + Assert.False(result); + } +} diff --git a/util/Migrator/DbScripts/2025-06-19_00_AddFiltersToOrganizationIntegrationConfiguration.sql b/util/Migrator/DbScripts/2025-06-19_00_AddFiltersToOrganizationIntegrationConfiguration.sql new file mode 100644 index 0000000000..686c46e514 --- /dev/null +++ b/util/Migrator/DbScripts/2025-06-19_00_AddFiltersToOrganizationIntegrationConfiguration.sql @@ -0,0 +1,104 @@ +/* add new column "Filters", nullable to OrganizationIntegrationConfiguration */ + +IF COL_LENGTH('[dbo].[OrganizationIntegrationConfiguration]', 'Filters') IS NULL + BEGIN + ALTER TABLE + [dbo].[OrganizationIntegrationConfiguration] + ADD + [Filters] VARCHAR (MAX) NULL + END +GO + +/* add column "Filters" to OrganizationIntegrationConfigurationDetailsView */ +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 + +/* add column "Filters" to OrganizationIntegrationConfigurationView */ +CREATE OR ALTER VIEW [dbo].[OrganizationIntegrationConfigurationView] +AS +SELECT + * +FROM + [dbo].[OrganizationIntegrationConfiguration] +GO + +/* add column to OrganizationIntegrationConfiguration_Create */ +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 + +/* add column to OrganizationIntegrationConfiguration_Update */ +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 diff --git a/util/MySqlMigrations/Migrations/20250619185012_AddFiltersToOrganizationIntegrationConfiguration.Designer.cs b/util/MySqlMigrations/Migrations/20250619185012_AddFiltersToOrganizationIntegrationConfiguration.Designer.cs new file mode 100644 index 0000000000..731289a770 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250619185012_AddFiltersToOrganizationIntegrationConfiguration.Designer.cs @@ -0,0 +1,3175 @@ +// +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("20250619185012_AddFiltersToOrganizationIntegrationConfiguration")] + partial class AddFiltersToOrganizationIntegrationConfiguration + { + /// + 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.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.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/20250619185012_AddFiltersToOrganizationIntegrationConfiguration.cs b/util/MySqlMigrations/Migrations/20250619185012_AddFiltersToOrganizationIntegrationConfiguration.cs new file mode 100644 index 0000000000..62e28a63b5 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250619185012_AddFiltersToOrganizationIntegrationConfiguration.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddFiltersToOrganizationIntegrationConfiguration : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Filters", + table: "OrganizationIntegrationConfiguration", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Filters", + table: "OrganizationIntegrationConfiguration"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index ed8f56f7e6..0f983f8f06 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -316,6 +316,9 @@ namespace Bit.MySqlMigrations.Migrations b.Property("EventType") .HasColumnType("int"); + b.Property("Filters") + .HasColumnType("longtext"); + b.Property("OrganizationIntegrationId") .HasColumnType("char(36)"); diff --git a/util/PostgresMigrations/Migrations/20250619184947_AddFiltersToOrganizationIntegrationConfiguration.Designer.cs b/util/PostgresMigrations/Migrations/20250619184947_AddFiltersToOrganizationIntegrationConfiguration.Designer.cs new file mode 100644 index 0000000000..0c445c92bb --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250619184947_AddFiltersToOrganizationIntegrationConfiguration.Designer.cs @@ -0,0 +1,3181 @@ +// +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("20250619184947_AddFiltersToOrganizationIntegrationConfiguration")] + partial class AddFiltersToOrganizationIntegrationConfiguration + { + /// + 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.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.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/20250619184947_AddFiltersToOrganizationIntegrationConfiguration.cs b/util/PostgresMigrations/Migrations/20250619184947_AddFiltersToOrganizationIntegrationConfiguration.cs new file mode 100644 index 0000000000..7faa197aae --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250619184947_AddFiltersToOrganizationIntegrationConfiguration.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddFiltersToOrganizationIntegrationConfiguration : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Filters", + table: "OrganizationIntegrationConfiguration", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Filters", + table: "OrganizationIntegrationConfiguration"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index aa283cf352..aaf6124d7b 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -319,6 +319,9 @@ namespace Bit.PostgresMigrations.Migrations b.Property("EventType") .HasColumnType("integer"); + b.Property("Filters") + .HasColumnType("text"); + b.Property("OrganizationIntegrationId") .HasColumnType("uuid"); diff --git a/util/SqliteMigrations/Migrations/20250619184959_AddFiltersToOrganizationIntegrationConfiguration.Designer.cs b/util/SqliteMigrations/Migrations/20250619184959_AddFiltersToOrganizationIntegrationConfiguration.Designer.cs new file mode 100644 index 0000000000..5cf399164c --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250619184959_AddFiltersToOrganizationIntegrationConfiguration.Designer.cs @@ -0,0 +1,3164 @@ +// +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("20250619184959_AddFiltersToOrganizationIntegrationConfiguration")] + partial class AddFiltersToOrganizationIntegrationConfiguration + { + /// + 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.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.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/20250619184959_AddFiltersToOrganizationIntegrationConfiguration.cs b/util/SqliteMigrations/Migrations/20250619184959_AddFiltersToOrganizationIntegrationConfiguration.cs new file mode 100644 index 0000000000..f526021ee3 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250619184959_AddFiltersToOrganizationIntegrationConfiguration.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddFiltersToOrganizationIntegrationConfiguration : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Filters", + table: "OrganizationIntegrationConfiguration", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Filters", + table: "OrganizationIntegrationConfiguration"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index ef9fb7cbfd..4bed680478 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -311,6 +311,9 @@ namespace Bit.SqliteMigrations.Migrations b.Property("EventType") .HasColumnType("INTEGER"); + b.Property("Filters") + .HasColumnType("TEXT"); + b.Property("OrganizationIntegrationId") .HasColumnType("TEXT"); 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 39/47] [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 40/47] [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 41/47] 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 42/47] [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 43/47] 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 44/47] 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 45/47] [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 46/47] [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 47/47] [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