From f6cc140fded360256429b24a378cbcd969e7d50a Mon Sep 17 00:00:00 2001 From: Jared McCannon <jmccannon@bitwarden.com> Date: Thu, 20 Mar 2025 09:12:39 -0500 Subject: [PATCH 01/19] Switched from .Any to Count. Remove unreachable code. (#5519) --- src/Api/Vault/Controllers/CiphersController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 62f07005ee..daaf8a03fb 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -127,9 +127,9 @@ public class CiphersController : Controller public async Task<ListResponseModel<CipherDetailsResponseModel>> Get() { var user = await _userService.GetUserByPrincipalAsync(User); - var hasOrgs = _currentContext.Organizations?.Any() ?? false; + var hasOrgs = _currentContext.Organizations.Count != 0; // TODO: Use hasOrgs proper for cipher listing here? - var ciphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: true || hasOrgs); + var ciphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: true); Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict = null; if (hasOrgs) { From bb674b8990499f0c6680fb5efb009c9316a99218 Mon Sep 17 00:00:00 2001 From: Github Actions <actions@github.com> Date: Thu, 20 Mar 2025 17:14:35 +0000 Subject: [PATCH 02/19] Bumped version to 2025.3.1 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index a994b2196e..082f85906d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ <PropertyGroup> <TargetFramework>net8.0</TargetFramework> - <Version>2025.3.0</Version> + <Version>2025.3.1</Version> <RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace> <ImplicitUsings>enable</ImplicitUsings> From 2d02ad3f61b939d08bc901e510007d8ed9ce2fe1 Mon Sep 17 00:00:00 2001 From: Github Actions <actions@github.com> Date: Thu, 20 Mar 2025 17:30:55 +0000 Subject: [PATCH 03/19] Bumped version to 2025.3.2 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 082f85906d..cbe9786d65 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ <PropertyGroup> <TargetFramework>net8.0</TargetFramework> - <Version>2025.3.1</Version> + <Version>2025.3.2</Version> <RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace> <ImplicitUsings>enable</ImplicitUsings> From 948d8f707d7d98506b6f87f4157721e8b2838c22 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 20 Mar 2025 14:41:58 -0500 Subject: [PATCH 04/19] [PM-18858] Security Task email bugs (#5536) * make "Review at-risk passwords" bold * add owner and admin email address to the bottom of the security notification email * fix plurality of text email --- .../SecurityTasksNotification.html.hbs | 11 ++++- .../SecurityTasksNotification.text.hbs | 9 ++++ .../Mail/SecurityTaskNotificationViewModel.cs | 2 + src/Core/Services/IMailService.cs | 2 +- .../Implementations/HandlebarsMailService.cs | 42 ++++++++++++++++++- .../NoopImplementations/NoopMailService.cs | 2 +- .../CreateManyTaskNotificationsCommand.cs | 10 ++++- 7 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.html.hbs b/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.html.hbs index 039806f44b..ca015e3e83 100644 --- a/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.html.hbs +++ b/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.html.hbs @@ -15,14 +15,21 @@ </tr> </table> <table width="100%" border="0" cellpadding="0" cellspacing="0" - style="display: table; width:100%; padding-bottom: 35px; text-align: center;" align="center"> + style="display: table; width:100%; padding-bottom: 24px; text-align: center;" align="center"> <tr> <td display="display: table-cell"> <a href="{{ReviewPasswordsUrl}}" clicktracking=off target="_blank" - style="display: inline-block; color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; border-radius: 999px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> + style="display: inline-block; font-weight: bold; color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; border-radius: 999px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> Review at-risk passwords </a> </td> </tr> +<table width="100%" border="0" cellpadding="0" cellspacing="0" + style="display: table; width:100%; padding-bottom: 24px; text-align: center;" align="center"> + <tr> + <td display="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 12px; line-height: 16px;"> + {{formatAdminOwnerEmails AdminOwnerEmails}} + </td> + </tr> </table> {{/SecurityTasksHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs b/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs index ba8650ad10..f5493e4503 100644 --- a/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs +++ b/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs @@ -5,4 +5,13 @@ breach. Launch the Bitwarden extension to review your at-risk passwords. Review at-risk passwords ({{{ReviewPasswordsUrl}}}) + +{{#if (eq (length AdminOwnerEmails) 1)}} +This request was initiated by {{AdminOwnerEmails.[0]}}. +{{else}} +This request was initiated by +{{#each AdminOwnerEmails}} + {{#if @last}}and {{/if}}{{this}}{{#unless @last}}, {{/unless}} +{{/each}}. +{{/if}} {{/SecurityTasksHtmlLayout}} diff --git a/src/Core/Models/Mail/SecurityTaskNotificationViewModel.cs b/src/Core/Models/Mail/SecurityTaskNotificationViewModel.cs index 7f93ac2439..8871a53424 100644 --- a/src/Core/Models/Mail/SecurityTaskNotificationViewModel.cs +++ b/src/Core/Models/Mail/SecurityTaskNotificationViewModel.cs @@ -8,5 +8,7 @@ public class SecurityTaskNotificationViewModel : BaseMailModel public bool TaskCountPlural => TaskCount != 1; + public IEnumerable<string> AdminOwnerEmails { get; set; } + public string ReviewPasswordsUrl => $"{WebVaultUrl}/browser-extension-prompt"; } diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 04b302bad9..e61127c57a 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -99,5 +99,5 @@ public interface IMailService string organizationName); Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList); Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable<string> adminEmails, Guid organizationId, string email, string userName); - Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications); + Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications, IEnumerable<string> adminOwnerEmails); } diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 588365f8c9..edb99809f7 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -740,6 +740,45 @@ public class HandlebarsMailService : IMailService var clickTrackingText = (clickTrackingOff ? "clicktracking=off" : string.Empty); writer.WriteSafeString($"<a href=\"{href}\" target=\"_blank\" {clickTrackingText}>{text}</a>"); }); + + // Construct markup for admin and owner email addresses. + // Using conditionals within the handlebar syntax was including extra spaces around + // concatenated strings, which this helper avoids. + Handlebars.RegisterHelper("formatAdminOwnerEmails", (writer, context, parameters) => + { + if (parameters.Length == 0) + { + writer.WriteSafeString(string.Empty); + return; + } + + var emailList = ((IEnumerable<string>)parameters[0]).ToList(); + if (emailList.Count == 0) + { + writer.WriteSafeString(string.Empty); + return; + } + + string constructAnchorElement(string email) + { + return $"<a style=\"color: #175DDC\" href=\"mailto:{email}\">{email}</a>"; + } + + var outputMessage = "This request was initiated by "; + + if (emailList.Count == 1) + { + outputMessage += $"{constructAnchorElement(emailList[0])}."; + } + else + { + outputMessage += string.Join(", ", emailList.Take(emailList.Count - 1) + .Select(email => constructAnchorElement(email))); + outputMessage += $", and {constructAnchorElement(emailList.Last())}."; + } + + writer.WriteSafeString($"{outputMessage}"); + }); } public async Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token) @@ -1201,7 +1240,7 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications) + public async Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications, IEnumerable<string> adminOwnerEmails) { MailQueueMessage CreateMessage(UserSecurityTasksCount notification) { @@ -1211,6 +1250,7 @@ public class HandlebarsMailService : IMailService { OrgName = CoreHelpers.SanitizeForEmail(sanitizedOrgName, false), TaskCount = notification.TaskCount, + AdminOwnerEmails = adminOwnerEmails, WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, }; message.Category = "SecurityTasksNotification"; diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 776dd07f19..d829fbbacb 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -324,7 +324,7 @@ public class NoopMailService : IMailService return Task.FromResult(0); } - public Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications) + public Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications, IEnumerable<string> adminOwnerEmails) { return Task.FromResult(0); } diff --git a/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs b/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs index f939816301..a335b059a4 100644 --- a/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs +++ b/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs @@ -17,19 +17,22 @@ public class CreateManyTaskNotificationsCommand : ICreateManyTaskNotificationsCo private readonly IMailService _mailService; private readonly ICreateNotificationCommand _createNotificationCommand; private readonly IPushNotificationService _pushNotificationService; + private readonly IOrganizationUserRepository _organizationUserRepository; public CreateManyTaskNotificationsCommand( IGetSecurityTasksNotificationDetailsQuery getSecurityTasksNotificationDetailsQuery, IOrganizationRepository organizationRepository, IMailService mailService, ICreateNotificationCommand createNotificationCommand, - IPushNotificationService pushNotificationService) + IPushNotificationService pushNotificationService, + IOrganizationUserRepository organizationUserRepository) { _getSecurityTasksNotificationDetailsQuery = getSecurityTasksNotificationDetailsQuery; _organizationRepository = organizationRepository; _mailService = mailService; _createNotificationCommand = createNotificationCommand; _pushNotificationService = pushNotificationService; + _organizationUserRepository = organizationUserRepository; } public async Task CreateAsync(Guid orgId, IEnumerable<SecurityTask> securityTasks) @@ -45,8 +48,11 @@ public class CreateManyTaskNotificationsCommand : ICreateManyTaskNotificationsCo }).ToList(); var organization = await _organizationRepository.GetByIdAsync(orgId); + var orgAdminEmails = await _organizationUserRepository.GetManyDetailsByRoleAsync(orgId, OrganizationUserType.Admin); + var orgOwnerEmails = await _organizationUserRepository.GetManyDetailsByRoleAsync(orgId, OrganizationUserType.Owner); + var orgAdminAndOwnerEmails = orgAdminEmails.Concat(orgOwnerEmails).Select(x => x.Email).Distinct().ToList(); - await _mailService.SendBulkSecurityTaskNotificationsAsync(organization, userTaskCount); + await _mailService.SendBulkSecurityTaskNotificationsAsync(organization, userTaskCount, orgAdminAndOwnerEmails); // Break securityTaskCiphers into separate lists by user Id var securityTaskCiphersByUser = securityTaskCiphers.GroupBy(x => x.UserId) From 5d549402c713f179781c9680952ac95186055934 Mon Sep 17 00:00:00 2001 From: Github Actions <actions@github.com> Date: Fri, 21 Mar 2025 10:15:22 +0000 Subject: [PATCH 05/19] Bumped version to 2025.3.3 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index cbe9786d65..2ede6ad8d1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ <PropertyGroup> <TargetFramework>net8.0</TargetFramework> - <Version>2025.3.2</Version> + <Version>2025.3.3</Version> <RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace> <ImplicitUsings>enable</ImplicitUsings> From c7c6528faaa2fb6f20b9e3e9186404405986abe0 Mon Sep 17 00:00:00 2001 From: Brandon Treston <btreston@bitwarden.com> Date: Fri, 21 Mar 2025 10:07:55 -0400 Subject: [PATCH 06/19] Ac/pm 18240 implement policy requirement for reset password policy (#5521) * wip * fix test * fix test * refactor * fix factory method and tests * cleanup * refactor * update copy * cleanup --- .../OrganizationUsersController.cs | 11 ++- .../Controllers/OrganizationsController.cs | 15 ++- .../ResetPasswordPolicyRequirement.cs | 46 ++++++++++ .../PolicyServiceCollectionExtensions.cs | 1 + .../Implementations/OrganizationService.cs | 29 ++++-- .../OrganizationUsersControllerTests.cs | 91 +++++++++++++++++++ .../OrganizationsControllerTests.cs | 57 ++++++++++++ ...etPasswordPolicyRequirementFactoryTests.cs | 37 ++++++++ 8 files changed, 277 insertions(+), 10 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/ResetPasswordPolicyRequirement.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/ResetPasswordPolicyRequirementFactoryTests.cs diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 5a73e57204..cc7f2314fd 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -8,6 +8,8 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; @@ -55,6 +57,7 @@ public class OrganizationUsersController : Controller private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand; private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery; + private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IFeatureService _featureService; private readonly IPricingClient _pricingClient; @@ -79,6 +82,7 @@ public class OrganizationUsersController : Controller IRemoveOrganizationUserCommand removeOrganizationUserCommand, IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand, IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery, + IPolicyRequirementQuery policyRequirementQuery, IFeatureService featureService, IPricingClient pricingClient) { @@ -102,6 +106,7 @@ public class OrganizationUsersController : Controller _removeOrganizationUserCommand = removeOrganizationUserCommand; _deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand; _getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery; + _policyRequirementQuery = policyRequirementQuery; _featureService = featureService; _pricingClient = pricingClient; } @@ -315,11 +320,13 @@ public class OrganizationUsersController : Controller throw new UnauthorizedAccessException(); } - var useMasterPasswordPolicy = await ShouldHandleResetPasswordAsync(orgId); + var useMasterPasswordPolicy = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements) + ? (await _policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id)).AutoEnrollEnabled(orgId) + : await ShouldHandleResetPasswordAsync(orgId); if (useMasterPasswordPolicy && string.IsNullOrWhiteSpace(model.ResetPasswordKey)) { - throw new BadRequestException(string.Empty, "Master Password reset is required, but not provided."); + throw new BadRequestException("Master Password reset is required, but not provided."); } await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService); diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 34da3de10c..9fa9cb6672 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -16,6 +16,8 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; @@ -61,6 +63,7 @@ public class OrganizationsController : Controller private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand; private readonly IOrganizationDeleteCommand _organizationDeleteCommand; + private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IPricingClient _pricingClient; public OrganizationsController( @@ -84,6 +87,7 @@ public class OrganizationsController : Controller IRemoveOrganizationUserCommand removeOrganizationUserCommand, ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand, IOrganizationDeleteCommand organizationDeleteCommand, + IPolicyRequirementQuery policyRequirementQuery, IPricingClient pricingClient) { _organizationRepository = organizationRepository; @@ -106,6 +110,7 @@ public class OrganizationsController : Controller _removeOrganizationUserCommand = removeOrganizationUserCommand; _cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand; _organizationDeleteCommand = organizationDeleteCommand; + _policyRequirementQuery = policyRequirementQuery; _pricingClient = pricingClient; } @@ -163,8 +168,13 @@ public class OrganizationsController : Controller throw new NotFoundException(); } - var resetPasswordPolicy = - await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword); + if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) + { + var resetPasswordPolicyRequirement = await _policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id); + return new OrganizationAutoEnrollStatusResponseModel(organization.Id, resetPasswordPolicyRequirement.AutoEnrollEnabled(organization.Id)); + } + + var resetPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword); if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled || resetPasswordPolicy.Data == null) { return new OrganizationAutoEnrollStatusResponseModel(organization.Id, false); @@ -172,6 +182,7 @@ public class OrganizationsController : Controller var data = JsonSerializer.Deserialize<ResetPasswordDataModel>(resetPasswordPolicy.Data, JsonHelpers.IgnoreCase); return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false); + } [HttpPost("")] diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/ResetPasswordPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/ResetPasswordPolicyRequirement.cs new file mode 100644 index 0000000000..4feef1b088 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/ResetPasswordPolicyRequirement.cs @@ -0,0 +1,46 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.Enums; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +/// <summary> +/// Policy requirements for the Account recovery administration policy. +/// </summary> +public class ResetPasswordPolicyRequirement : IPolicyRequirement +{ + /// <summary> + /// List of Organization Ids that require automatic enrollment in password recovery. + /// </summary> + private IEnumerable<Guid> _autoEnrollOrganizations; + public IEnumerable<Guid> AutoEnrollOrganizations { init => _autoEnrollOrganizations = value; } + + /// <summary> + /// Returns true if provided organizationId requires automatic enrollment in password recovery. + /// </summary> + public bool AutoEnrollEnabled(Guid organizationId) + { + return _autoEnrollOrganizations.Contains(organizationId); + } + + +} + +public class ResetPasswordPolicyRequirementFactory : BasePolicyRequirementFactory<ResetPasswordPolicyRequirement> +{ + public override PolicyType PolicyType => PolicyType.ResetPassword; + + protected override bool ExemptProviders => false; + + protected override IEnumerable<OrganizationUserType> ExemptRoles => []; + + public override ResetPasswordPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails) + { + var result = policyDetails + .Where(p => p.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled) + .Select(p => p.OrganizationId) + .ToHashSet(); + + return new ResetPasswordPolicyRequirement() { AutoEnrollOrganizations = result }; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index 6c698f9ffc..d386006ad2 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -33,5 +33,6 @@ public static class PolicyServiceCollectionExtensions { services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, DisableSendPolicyRequirementFactory>(); services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SendOptionsPolicyRequirementFactory>(); + services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, ResetPasswordPolicyRequirementFactory>(); } } diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 1b44eea496..772b407951 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -6,6 +6,8 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; @@ -76,6 +78,7 @@ public class OrganizationService : IOrganizationService private readonly IOrganizationBillingService _organizationBillingService; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; private readonly IPricingClient _pricingClient; + private readonly IPolicyRequirementQuery _policyRequirementQuery; public OrganizationService( IOrganizationRepository organizationRepository, @@ -111,7 +114,8 @@ public class OrganizationService : IOrganizationService ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IOrganizationBillingService organizationBillingService, IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, - IPricingClient pricingClient) + IPricingClient pricingClient, + IPolicyRequirementQuery policyRequirementQuery) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -147,6 +151,7 @@ public class OrganizationService : IOrganizationService _organizationBillingService = organizationBillingService; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; _pricingClient = pricingClient; + _policyRequirementQuery = policyRequirementQuery; } public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, @@ -1353,13 +1358,25 @@ public class OrganizationService : IOrganizationService } // Block the user from withdrawal if auto enrollment is enabled - if (resetPasswordKey == null && resetPasswordPolicy.Data != null) + if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) { - var data = JsonSerializer.Deserialize<ResetPasswordDataModel>(resetPasswordPolicy.Data, JsonHelpers.IgnoreCase); - - if (data?.AutoEnrollEnabled ?? false) + var resetPasswordPolicyRequirement = await _policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(userId); + if (resetPasswordKey == null && resetPasswordPolicyRequirement.AutoEnrollEnabled(organizationId)) { - throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to withdraw from Password Reset."); + 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<ResetPasswordDataModel>(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."); + } } } diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs index e3071bd227..a19560ecee 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs @@ -7,6 +7,8 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Repositories; @@ -424,4 +426,93 @@ public class OrganizationUsersControllerTests .GetManyDetailsByOrganizationAsync(organizationAbility.Id, Arg.Any<bool>(), Arg.Any<bool>()) .Returns(organizationUsers); } + + [Theory] + [BitAutoData] + public async Task Accept_WhenOrganizationUsePoliciesIsEnabledAndResetPolicyIsEnabled_WithPolicyRequirementsEnabled_ShouldHandleResetPassword(Guid orgId, Guid orgUserId, + OrganizationUserAcceptRequestModel model, User user, SutProvider<OrganizationUsersController> sutProvider) + { + // Arrange + var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>(); + applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = true }); + + sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + + var policy = new Policy + { + Enabled = true, + Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }), + }; + var userService = sutProvider.GetDependency<IUserService>(); + userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user); + + var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>(); + + var policyRepository = sutProvider.GetDependency<IPolicyRepository>(); + + var policyRequirement = new ResetPasswordPolicyRequirement { AutoEnrollOrganizations = [orgId] }; + + policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id).Returns(policyRequirement); + + // Act + await sutProvider.Sut.Accept(orgId, orgUserId, model); + + // Assert + await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1) + .AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService); + await sutProvider.GetDependency<IOrganizationService>().Received(1) + .UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id); + + await userService.Received(1).GetUserByPrincipalAsync(default); + await applicationCacheService.Received(0).GetOrganizationAbilityAsync(orgId); + await policyRepository.Received(0).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword); + await policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id); + Assert.True(policyRequirement.AutoEnrollEnabled(orgId)); + } + + [Theory] + [BitAutoData] + public async Task Accept_WithInvalidModelResetPasswordKey_WithPolicyRequirementsEnabled_ThrowsBadRequestException(Guid orgId, Guid orgUserId, + OrganizationUserAcceptRequestModel model, User user, SutProvider<OrganizationUsersController> sutProvider) + { + // Arrange + model.ResetPasswordKey = " "; + var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>(); + applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = true }); + + sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + + var policy = new Policy + { + Enabled = true, + Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }), + }; + var userService = sutProvider.GetDependency<IUserService>(); + userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user); + + var policyRepository = sutProvider.GetDependency<IPolicyRepository>(); + + var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>(); + + var policyRequirement = new ResetPasswordPolicyRequirement { AutoEnrollOrganizations = [orgId] }; + + policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id).Returns(policyRequirement); + + // Act + var exception = await Assert.ThrowsAsync<BadRequestException>(() => + sutProvider.Sut.Accept(orgId, orgUserId, model)); + + // Assert + await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(0) + .AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService); + await sutProvider.GetDependency<IOrganizationService>().Received(0) + .UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id); + + await userService.Received(1).GetUserByPrincipalAsync(default); + await applicationCacheService.Received(0).GetOrganizationAbilityAsync(orgId); + await policyRepository.Received(0).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword); + await policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id); + + Assert.Equal("Master Password reset is required, but not provided.", exception.Message); + } } diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index b0906ddc43..8e6d2ce27b 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -4,12 +4,15 @@ using Bit.Api.AdminConsole.Controllers; using Bit.Api.Auth.Models.Request.Accounts; using Bit.Core; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; @@ -55,6 +58,7 @@ public class OrganizationsControllerTests : IDisposable private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand; private readonly IOrganizationDeleteCommand _organizationDeleteCommand; + private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IPricingClient _pricingClient; private readonly OrganizationsController _sut; @@ -80,6 +84,7 @@ public class OrganizationsControllerTests : IDisposable _removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>(); _cloudOrganizationSignUpCommand = Substitute.For<ICloudOrganizationSignUpCommand>(); _organizationDeleteCommand = Substitute.For<IOrganizationDeleteCommand>(); + _policyRequirementQuery = Substitute.For<IPolicyRequirementQuery>(); _pricingClient = Substitute.For<IPricingClient>(); _sut = new OrganizationsController( @@ -103,6 +108,7 @@ public class OrganizationsControllerTests : IDisposable _removeOrganizationUserCommand, _cloudOrganizationSignUpCommand, _organizationDeleteCommand, + _policyRequirementQuery, _pricingClient); } @@ -236,4 +242,55 @@ public class OrganizationsControllerTests : IDisposable await _organizationDeleteCommand.Received(1).DeleteAsync(organization); } + + [Theory, AutoData] + public async Task GetAutoEnrollStatus_WithPolicyRequirementsEnabled_ReturnsOrganizationAutoEnrollStatus_WithResetPasswordEnabledTrue( + User user, + Organization organization, + OrganizationUser organizationUser + ) + { + var policyRequirement = new ResetPasswordPolicyRequirement() { AutoEnrollOrganizations = [organization.Id] }; + + _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); + _organizationRepository.GetByIdentifierAsync(organization.Id.ToString()).Returns(organization); + _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id).Returns(organizationUser); + _policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id).Returns(policyRequirement); + + var result = await _sut.GetAutoEnrollStatus(organization.Id.ToString()); + + await _userService.Received(1).GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()); + await _organizationRepository.Received(1).GetByIdentifierAsync(organization.Id.ToString()); + await _policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id); + + Assert.True(result.ResetPasswordEnabled); + Assert.Equal(result.Id, organization.Id); + } + + [Theory, AutoData] + public async Task GetAutoEnrollStatus_WithPolicyRequirementsDisabled_ReturnsOrganizationAutoEnrollStatus_WithResetPasswordEnabledTrue( + User user, + Organization organization, + OrganizationUser organizationUser +) + { + + var policy = new Policy() { Type = PolicyType.ResetPassword, Enabled = true, Data = "{\"AutoEnrollEnabled\": true}", OrganizationId = organization.Id }; + + _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); + _organizationRepository.GetByIdentifierAsync(organization.Id.ToString()).Returns(organization); + _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(false); + _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id).Returns(organizationUser); + _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword).Returns(policy); + + var result = await _sut.GetAutoEnrollStatus(organization.Id.ToString()); + + await _userService.Received(1).GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()); + await _organizationRepository.Received(1).GetByIdentifierAsync(organization.Id.ToString()); + await _policyRequirementQuery.Received(0).GetAsync<ResetPasswordPolicyRequirement>(user.Id); + await _policyRepository.Received(1).GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword); + + Assert.True(result.ResetPasswordEnabled); + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/ResetPasswordPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/ResetPasswordPolicyRequirementFactoryTests.cs new file mode 100644 index 0000000000..181f4f170e --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/ResetPasswordPolicyRequirementFactoryTests.cs @@ -0,0 +1,37 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +[SutProviderCustomize] +public class ResetPasswordPolicyRequirementFactoryTests +{ + [Theory, BitAutoData] + public void AutoEnroll_WithNoPolicies_IsEmpty(SutProvider<ResetPasswordPolicyRequirementFactory> sutProvider, Guid orgId) + { + var actual = sutProvider.Sut.Create([]); + + Assert.False(actual.AutoEnrollEnabled(orgId)); + } + + [Theory, BitAutoData] + public void AutoEnrollAdministration_WithAnyResetPasswordPolices_ReturnsEnabledOrganizationIds( + [PolicyDetails(PolicyType.ResetPassword)] PolicyDetails[] policies, + SutProvider<ResetPasswordPolicyRequirementFactory> sutProvider) + { + policies[0].SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true }); + policies[1].SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = false }); + policies[2].SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.True(actual.AutoEnrollEnabled(policies[0].OrganizationId)); + Assert.False(actual.AutoEnrollEnabled(policies[1].OrganizationId)); + Assert.True(actual.AutoEnrollEnabled(policies[2].OrganizationId)); + } +} From 7eb8ad8fa3a0d9975da0722874f777738d818cfe Mon Sep 17 00:00:00 2001 From: Github Actions <actions@github.com> Date: Mon, 24 Mar 2025 10:49:33 +0000 Subject: [PATCH 07/19] Bumped version to 2025.3.4 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 2ede6ad8d1..0ef5513003 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ <PropertyGroup> <TargetFramework>net8.0</TargetFramework> - <Version>2025.3.3</Version> + <Version>2025.3.4</Version> <RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace> <ImplicitUsings>enable</ImplicitUsings> From 1db37a14abdb20a06ebe853de1aaa569782c1d13 Mon Sep 17 00:00:00 2001 From: Github Actions <actions@github.com> Date: Mon, 24 Mar 2025 10:56:04 +0000 Subject: [PATCH 08/19] Bumped version to 2025.3.5 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 0ef5513003..0d17b69b55 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ <PropertyGroup> <TargetFramework>net8.0</TargetFramework> - <Version>2025.3.4</Version> + <Version>2025.3.5</Version> <RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace> <ImplicitUsings>enable</ImplicitUsings> From d345937ecca07aefd07f8cea421ae7ca2b6fcf98 Mon Sep 17 00:00:00 2001 From: Github Actions <actions@github.com> Date: Mon, 24 Mar 2025 11:22:29 +0000 Subject: [PATCH 09/19] Bumped version to 2025.3.6 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 0d17b69b55..b9b8cf557a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ <PropertyGroup> <TargetFramework>net8.0</TargetFramework> - <Version>2025.3.5</Version> + <Version>2025.3.6</Version> <RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace> <ImplicitUsings>enable</ImplicitUsings> From 24b63f2dcd12e1268fa625c8e1bef3e16c2aa17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Mon, 24 Mar 2025 17:05:46 +0000 Subject: [PATCH 10/19] [PM-12493] Extract ConfirmUser methods from OrganizationService into commands (#5505) * Add ConfirmOrganizationUserCommand and IConfirmOrganizationUserCommand interface for managing organization user confirmations * Add unit tests for ConfirmOrganizationUserCommand to validate user confirmation scenarios * Register ConfirmOrganizationUserCommand for dependency injection * Refactor OrganizationUsersController to utilize IConfirmOrganizationUserCommand for user confirmation processes * Remove ConfirmUserAsync and ConfirmUsersAsync methods from IOrganizationService and OrganizationService * Rename test methods in ConfirmOrganizationUserCommandTests for clarity and consistency * Update test method name in ConfirmOrganizationUserCommandTests for improved clarity --- .../OrganizationUsersController.cs | 11 +- .../ConfirmOrganizationUserCommand.cs | 186 ++++++++++ .../IConfirmOrganizationUserCommand.cs | 30 ++ .../Services/IOrganizationService.cs | 3 - .../Implementations/OrganizationService.cs | 126 ------- ...OrganizationServiceCollectionExtensions.cs | 1 + .../ConfirmOrganizationUserCommandTests.cs | 324 ++++++++++++++++++ .../Services/OrganizationServiceTests.cs | 301 ---------------- 8 files changed, 548 insertions(+), 434 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index cc7f2314fd..cfe93e87ce 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -60,6 +60,7 @@ public class OrganizationUsersController : Controller private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IFeatureService _featureService; private readonly IPricingClient _pricingClient; + private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand; public OrganizationUsersController( IOrganizationRepository organizationRepository, @@ -84,7 +85,8 @@ public class OrganizationUsersController : Controller IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery, IPolicyRequirementQuery policyRequirementQuery, IFeatureService featureService, - IPricingClient pricingClient) + IPricingClient pricingClient, + IConfirmOrganizationUserCommand confirmOrganizationUserCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -109,6 +111,7 @@ public class OrganizationUsersController : Controller _policyRequirementQuery = policyRequirementQuery; _featureService = featureService; _pricingClient = pricingClient; + _confirmOrganizationUserCommand = confirmOrganizationUserCommand; } [HttpGet("{id}")] @@ -308,7 +311,7 @@ public class OrganizationUsersController : Controller await _organizationService.InitPendingOrganization(user.Id, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName); await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService); - await _organizationService.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id); + await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id); } [HttpPost("{organizationUserId}/accept")] @@ -364,7 +367,7 @@ public class OrganizationUsersController : Controller } var userId = _userService.GetProperUserId(User); - var result = await _organizationService.ConfirmUserAsync(orgGuidId, new Guid(id), model.Key, userId.Value); + var result = await _confirmOrganizationUserCommand.ConfirmUserAsync(orgGuidId, new Guid(id), model.Key, userId.Value); } [HttpPost("confirm")] @@ -378,7 +381,7 @@ public class OrganizationUsersController : Controller } var userId = _userService.GetProperUserId(User); - var results = await _organizationService.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value); + var results = await _confirmOrganizationUserCommand.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value); return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r => new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2))); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs new file mode 100644 index 0000000000..9bfe8f791e --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs @@ -0,0 +1,186 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.Services; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Billing.Enums; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; +using Bit.Core.Repositories; +using Bit.Core.Services; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; + +public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand +{ + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IUserRepository _userRepository; + private readonly IEventService _eventService; + private readonly IMailService _mailService; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IPushNotificationService _pushNotificationService; + private readonly IPushRegistrationService _pushRegistrationService; + private readonly IPolicyService _policyService; + private readonly IDeviceRepository _deviceRepository; + + public ConfirmOrganizationUserCommand( + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IUserRepository userRepository, + IEventService eventService, + IMailService mailService, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + IPushNotificationService pushNotificationService, + IPushRegistrationService pushRegistrationService, + IPolicyService policyService, + IDeviceRepository deviceRepository) + { + _organizationRepository = organizationRepository; + _organizationUserRepository = organizationUserRepository; + _userRepository = userRepository; + _eventService = eventService; + _mailService = mailService; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _pushNotificationService = pushNotificationService; + _pushRegistrationService = pushRegistrationService; + _policyService = policyService; + _deviceRepository = deviceRepository; + } + + public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, + Guid confirmingUserId) + { + var result = await ConfirmUsersAsync( + organizationId, + new Dictionary<Guid, string>() { { organizationUserId, key } }, + confirmingUserId); + + if (!result.Any()) + { + throw new BadRequestException("User not valid."); + } + + var (orgUser, error) = result[0]; + if (error != "") + { + throw new BadRequestException(error); + } + return orgUser; + } + + public async Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys, + Guid confirmingUserId) + { + var selectedOrganizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys); + var validSelectedOrganizationUsers = selectedOrganizationUsers + .Where(u => u.Status == OrganizationUserStatusType.Accepted && u.OrganizationId == organizationId && u.UserId != null) + .ToList(); + + if (!validSelectedOrganizationUsers.Any()) + { + return new List<Tuple<OrganizationUser, string>>(); + } + + var validSelectedUserIds = validSelectedOrganizationUsers.Select(u => u.UserId.Value).ToList(); + + var organization = await _organizationRepository.GetByIdAsync(organizationId); + var allUsersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validSelectedUserIds); + var users = await _userRepository.GetManyAsync(validSelectedUserIds); + var usersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(validSelectedUserIds); + + var keyedFilteredUsers = validSelectedOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u); + var keyedOrganizationUsers = allUsersOrgs.GroupBy(u => u.UserId.Value) + .ToDictionary(u => u.Key, u => u.ToList()); + + var succeededUsers = new List<OrganizationUser>(); + var result = new List<Tuple<OrganizationUser, string>>(); + + foreach (var user in users) + { + if (!keyedFilteredUsers.ContainsKey(user.Id)) + { + continue; + } + var orgUser = keyedFilteredUsers[user.Id]; + var orgUsers = keyedOrganizationUsers.GetValueOrDefault(user.Id, new List<OrganizationUser>()); + try + { + if (organization.PlanType == PlanType.Free && (orgUser.Type == OrganizationUserType.Admin + || orgUser.Type == 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(user.Id); + if (adminCount > 0) + { + throw new BadRequestException("User can only be an admin of one free organization."); + } + } + + var twoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled; + await CheckPoliciesAsync(organizationId, user, orgUsers, twoFactorEnabled); + orgUser.Status = OrganizationUserStatusType.Confirmed; + orgUser.Key = keys[orgUser.Id]; + orgUser.Email = null; + + await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); + await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, orgUser.AccessSecretsManager); + await DeleteAndPushUserRegistrationAsync(organizationId, user.Id); + succeededUsers.Add(orgUser); + result.Add(Tuple.Create(orgUser, "")); + } + catch (BadRequestException e) + { + result.Add(Tuple.Create(orgUser, e.Message)); + } + } + + await _organizationUserRepository.ReplaceManyAsync(succeededUsers); + + return result; + } + + private async Task CheckPoliciesAsync(Guid organizationId, User user, + ICollection<OrganizationUser> userOrgs, bool twoFactorEnabled) + { + // Enforce Two Factor Authentication Policy for this organization + var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)) + .Any(p => p.OrganizationId == organizationId); + if (orgRequiresTwoFactor && !twoFactorEnabled) + { + throw new BadRequestException("User does not have two-step login enabled."); + } + + var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId); + var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg); + var otherSingleOrgPolicies = + singleOrgPolicies.Where(p => p.OrganizationId != organizationId); + // Enforce Single Organization Policy for this organization + if (hasOtherOrgs && singleOrgPolicies.Any(p => p.OrganizationId == organizationId)) + { + throw new BadRequestException("Cannot confirm this member to the organization until they leave or remove all other organizations."); + } + // Enforce Single Organization Policy of other organizations user is a member of + if (otherSingleOrgPolicies.Any()) + { + throw new BadRequestException("Cannot confirm this member to the organization because they are in another organization which forbids it."); + } + } + + private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId) + { + var devices = await GetUserDeviceIdsAsync(userId); + await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices, + organizationId.ToString()); + await _pushNotificationService.PushSyncOrgKeysAsync(userId); + } + + private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId) + { + var devices = await _deviceRepository.GetManyByUserIdAsync(userId); + return devices + .Where(d => !string.IsNullOrWhiteSpace(d.PushToken)) + .Select(d => d.Id.ToString()); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs new file mode 100644 index 0000000000..302ee0901d --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs @@ -0,0 +1,30 @@ +using Bit.Core.Entities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; + +/// <summary> +/// Command to confirm organization users who have accepted their invitations. +/// </summary> +public interface IConfirmOrganizationUserCommand +{ + /// <summary> + /// Confirms a single organization user who has accepted their invitation. + /// </summary> + /// <param name="organizationId">The ID of the organization.</param> + /// <param name="organizationUserId">The ID of the organization user to confirm.</param> + /// <param name="key">The encrypted organization key for the user.</param> + /// <param name="confirmingUserId">The ID of the user performing the confirmation.</param> + /// <returns>The confirmed organization user.</returns> + /// <exception cref="BadRequestException">Thrown when the user is not valid or cannot be confirmed.</exception> + Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId); + + /// <summary> + /// Confirms multiple organization users who have accepted their invitations. + /// </summary> + /// <param name="organizationId">The ID of the organization.</param> + /// <param name="keys">A dictionary mapping organization user IDs to their encrypted organization keys.</param> + /// <param name="confirmingUserId">The ID of the user performing the confirmation.</param> + /// <returns>A list of tuples containing the organization user and an error message (if any).</returns> + Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys, + Guid confirmingUserId); +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index dacb2ab162..476fccb480 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -38,9 +38,6 @@ public interface IOrganizationService IEnumerable<(OrganizationUserInvite invite, string externalId)> invites); Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId); Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false); - Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId); - Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys, - Guid confirmingUserId); Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId); Task ImportAsync(Guid organizationId, IEnumerable<ImportedGroup> groups, IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<string> removeUserExternalIds, diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 772b407951..ab5703eaa1 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -1127,98 +1127,6 @@ public class OrganizationService : IOrganizationService ); } - public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, - Guid confirmingUserId) - { - var result = await ConfirmUsersAsync( - organizationId, - new Dictionary<Guid, string>() { { organizationUserId, key } }, - confirmingUserId); - - if (!result.Any()) - { - throw new BadRequestException("User not valid."); - } - - var (orgUser, error) = result[0]; - if (error != "") - { - throw new BadRequestException(error); - } - return orgUser; - } - - public async Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys, - Guid confirmingUserId) - { - var selectedOrganizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys); - var validSelectedOrganizationUsers = selectedOrganizationUsers - .Where(u => u.Status == OrganizationUserStatusType.Accepted && u.OrganizationId == organizationId && u.UserId != null) - .ToList(); - - if (!validSelectedOrganizationUsers.Any()) - { - return new List<Tuple<OrganizationUser, string>>(); - } - - var validSelectedUserIds = validSelectedOrganizationUsers.Select(u => u.UserId.Value).ToList(); - - var organization = await GetOrgById(organizationId); - var allUsersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validSelectedUserIds); - var users = await _userRepository.GetManyAsync(validSelectedUserIds); - var usersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(validSelectedUserIds); - - var keyedFilteredUsers = validSelectedOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u); - var keyedOrganizationUsers = allUsersOrgs.GroupBy(u => u.UserId.Value) - .ToDictionary(u => u.Key, u => u.ToList()); - - var succeededUsers = new List<OrganizationUser>(); - var result = new List<Tuple<OrganizationUser, string>>(); - - foreach (var user in users) - { - if (!keyedFilteredUsers.ContainsKey(user.Id)) - { - continue; - } - var orgUser = keyedFilteredUsers[user.Id]; - var orgUsers = keyedOrganizationUsers.GetValueOrDefault(user.Id, new List<OrganizationUser>()); - try - { - if (organization.PlanType == PlanType.Free && (orgUser.Type == OrganizationUserType.Admin - || orgUser.Type == 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(user.Id); - if (adminCount > 0) - { - throw new BadRequestException("User can only be an admin of one free organization."); - } - } - - var twoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled; - await CheckPoliciesAsync(organizationId, user, orgUsers, twoFactorEnabled); - orgUser.Status = OrganizationUserStatusType.Confirmed; - orgUser.Key = keys[orgUser.Id]; - orgUser.Email = null; - - await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); - await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, orgUser.AccessSecretsManager); - await DeleteAndPushUserRegistrationAsync(organizationId, user.Id); - succeededUsers.Add(orgUser); - result.Add(Tuple.Create(orgUser, "")); - } - catch (BadRequestException e) - { - result.Add(Tuple.Create(orgUser, e.Message)); - } - } - - await _organizationUserRepository.ReplaceManyAsync(succeededUsers); - - return result; - } - internal async Task<(bool canScale, string failureReason)> CanScaleAsync( Organization organization, int seatsToAdd) @@ -1305,32 +1213,7 @@ public class OrganizationService : IOrganizationService } } - private async Task CheckPoliciesAsync(Guid organizationId, User user, - ICollection<OrganizationUser> userOrgs, bool twoFactorEnabled) - { - // Enforce Two Factor Authentication Policy for this organization - var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)) - .Any(p => p.OrganizationId == organizationId); - if (orgRequiresTwoFactor && !twoFactorEnabled) - { - throw new BadRequestException("User does not have two-step login enabled."); - } - var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId); - var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg); - var otherSingleOrgPolicies = - singleOrgPolicies.Where(p => p.OrganizationId != organizationId); - // Enforce Single Organization Policy for this organization - if (hasOtherOrgs && singleOrgPolicies.Any(p => p.OrganizationId == organizationId)) - { - throw new BadRequestException("Cannot confirm this member to the organization until they leave or remove all other organizations."); - } - // Enforce Single Organization Policy of other organizations user is a member of - if (otherSingleOrgPolicies.Any()) - { - throw new BadRequestException("Cannot confirm this member to the organization because they are in another organization which forbids it."); - } - } public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId) { @@ -1640,15 +1523,6 @@ public class OrganizationService : IOrganizationService await _groupRepository.UpdateUsersAsync(group.Id, users); } - private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId) - { - var devices = await GetUserDeviceIdsAsync(userId); - await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices, - organizationId.ToString()); - await _pushNotificationService.PushSyncOrgKeysAsync(userId); - } - - private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId) { var devices = await _deviceRepository.GetManyByUserIdAsync(userId); diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 232e04fbd0..e13a06f660 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -116,6 +116,7 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped<IUpdateOrganizationUserCommand, UpdateOrganizationUserCommand>(); services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>(); services.AddScoped<IDeleteManagedOrganizationUserAccountCommand, DeleteManagedOrganizationUserAccountCommand>(); + services.AddScoped<IConfirmOrganizationUserCommand, ConfirmOrganizationUserCommand>(); } private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs new file mode 100644 index 0000000000..06335f668d --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs @@ -0,0 +1,324 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +using Bit.Core.AdminConsole.Services; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Billing.Enums; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers; + +[SutProviderCustomize] +public class ConfirmOrganizationUserCommandTests +{ + [Theory, BitAutoData] + public async Task ConfirmUserAsync_WithInvalidStatus_ThrowsBadRequestException(OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser orgUser, string key, + SutProvider<ConfirmOrganizationUserCommand> sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); + + organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser); + + var exception = await Assert.ThrowsAsync<BadRequestException>( + () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); + Assert.Contains("User not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_WithWrongOrganization_ThrowsBadRequestException(OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, string key, + SutProvider<ConfirmOrganizationUserCommand> sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); + + organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser); + + var exception = await Assert.ThrowsAsync<BadRequestException>( + () => sutProvider.Sut.ConfirmUserAsync(confirmingUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); + Assert.Contains("User not valid.", exception.Message); + } + + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task ConfirmUserAsync_ToFree_WithExistingAdminOrOwner_ThrowsBadRequestException(OrganizationUserType userType, Organization org, OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, + string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); + var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); + var userRepository = sutProvider.GetDependency<IUserRepository>(); + + org.PlanType = PlanType.Free; + orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; + orgUser.UserId = user.Id; + orgUser.Type = userType; + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); + organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(orgUser.UserId.Value).Returns(1); + organizationRepository.GetByIdAsync(org.Id).Returns(org); + userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); + + var exception = await Assert.ThrowsAsync<BadRequestException>( + () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); + Assert.Contains("User can only be an admin of one free organization.", exception.Message); + } + + [Theory] + [BitAutoData(PlanType.Custom, OrganizationUserType.Admin)] + [BitAutoData(PlanType.Custom, OrganizationUserType.Owner)] + [BitAutoData(PlanType.EnterpriseAnnually, OrganizationUserType.Admin)] + [BitAutoData(PlanType.EnterpriseAnnually, OrganizationUserType.Owner)] + [BitAutoData(PlanType.EnterpriseAnnually2020, OrganizationUserType.Admin)] + [BitAutoData(PlanType.EnterpriseAnnually2020, OrganizationUserType.Owner)] + [BitAutoData(PlanType.EnterpriseAnnually2019, OrganizationUserType.Admin)] + [BitAutoData(PlanType.EnterpriseAnnually2019, OrganizationUserType.Owner)] + [BitAutoData(PlanType.EnterpriseMonthly, OrganizationUserType.Admin)] + [BitAutoData(PlanType.EnterpriseMonthly, OrganizationUserType.Owner)] + [BitAutoData(PlanType.EnterpriseMonthly2020, OrganizationUserType.Admin)] + [BitAutoData(PlanType.EnterpriseMonthly2020, OrganizationUserType.Owner)] + [BitAutoData(PlanType.EnterpriseMonthly2019, OrganizationUserType.Admin)] + [BitAutoData(PlanType.EnterpriseMonthly2019, OrganizationUserType.Owner)] + [BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Admin)] + [BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Owner)] + [BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Admin)] + [BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Owner)] + [BitAutoData(PlanType.TeamsAnnually, OrganizationUserType.Admin)] + [BitAutoData(PlanType.TeamsAnnually, OrganizationUserType.Owner)] + [BitAutoData(PlanType.TeamsAnnually2020, OrganizationUserType.Admin)] + [BitAutoData(PlanType.TeamsAnnually2020, OrganizationUserType.Owner)] + [BitAutoData(PlanType.TeamsAnnually2019, OrganizationUserType.Admin)] + [BitAutoData(PlanType.TeamsAnnually2019, OrganizationUserType.Owner)] + [BitAutoData(PlanType.TeamsMonthly, OrganizationUserType.Admin)] + [BitAutoData(PlanType.TeamsMonthly, OrganizationUserType.Owner)] + [BitAutoData(PlanType.TeamsMonthly2020, OrganizationUserType.Admin)] + [BitAutoData(PlanType.TeamsMonthly2020, OrganizationUserType.Owner)] + [BitAutoData(PlanType.TeamsMonthly2019, OrganizationUserType.Admin)] + [BitAutoData(PlanType.TeamsMonthly2019, OrganizationUserType.Owner)] + public async Task ConfirmUserAsync_ToNonFree_WithExistingFreeAdminOrOwner_Succeeds(PlanType planType, OrganizationUserType orgUserType, Organization org, OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, + string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); + var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); + var userRepository = sutProvider.GetDependency<IUserRepository>(); + + org.PlanType = planType; + orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; + orgUser.UserId = user.Id; + orgUser.Type = orgUserType; + orgUser.AccessSecretsManager = false; + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); + organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(orgUser.UserId.Value).Returns(1); + organizationRepository.GetByIdAsync(org.Id).Returns(org); + userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); + + await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id); + + await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); + await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email); + await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1)); + } + + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_AsUser_WithSingleOrgPolicyAppliedFromConfirmingOrg_ThrowsBadRequestException(Organization org, OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, + OrganizationUser orgUserAnotherOrg, [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy, + string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); + var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); + var userRepository = sutProvider.GetDependency<IUserRepository>(); + var policyService = sutProvider.GetDependency<IPolicyService>(); + + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.Status = OrganizationUserStatusType.Accepted; + orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; + orgUser.UserId = orgUserAnotherOrg.UserId = user.Id; + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); + organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg }); + organizationRepository.GetByIdAsync(org.Id).Returns(org); + userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); + singleOrgPolicy.OrganizationId = org.Id; + policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy }); + + var exception = await Assert.ThrowsAsync<BadRequestException>( + () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); + Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", exception.Message); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_AsUser_WithSingleOrgPolicyAppliedFromOtherOrg_ThrowsBadRequestException(Organization org, OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, + OrganizationUser orgUserAnotherOrg, [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy, + string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); + var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); + var userRepository = sutProvider.GetDependency<IUserRepository>(); + var policyService = sutProvider.GetDependency<IPolicyService>(); + + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.Status = OrganizationUserStatusType.Accepted; + orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; + orgUser.UserId = orgUserAnotherOrg.UserId = user.Id; + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); + organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg }); + organizationRepository.GetByIdAsync(org.Id).Returns(org); + userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); + singleOrgPolicy.OrganizationId = orgUserAnotherOrg.Id; + policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy }); + + var exception = await Assert.ThrowsAsync<BadRequestException>( + () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); + Assert.Contains("Cannot confirm this member to the organization because they are in another organization which forbids it.", exception.Message); + } + + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task ConfirmUserAsync_AsOwnerOrAdmin_WithSingleOrgPolicy_ExcludedViaUserType_Success( + OrganizationUserType userType, Organization org, OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, + OrganizationUser orgUserAnotherOrg, + string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); + var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); + var userRepository = sutProvider.GetDependency<IUserRepository>(); + + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.Type = userType; + orgUser.Status = OrganizationUserStatusType.Accepted; + orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; + orgUser.UserId = orgUserAnotherOrg.UserId = user.Id; + orgUser.AccessSecretsManager = true; + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); + organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg }); + organizationRepository.GetByIdAsync(org.Id).Returns(org); + userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); + + await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id); + + await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); + await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, true); + await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1)); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_WithTwoFactorPolicyAndTwoFactorDisabled_ThrowsBadRequestException(Organization org, OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, + OrganizationUser orgUserAnotherOrg, + [OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy, + string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); + var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); + var userRepository = sutProvider.GetDependency<IUserRepository>(); + var policyService = sutProvider.GetDependency<IPolicyService>(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(); + + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; + orgUser.UserId = orgUserAnotherOrg.UserId = user.Id; + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); + organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg }); + organizationRepository.GetByIdAsync(org.Id).Returns(org); + userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); + twoFactorPolicy.OrganizationId = org.Id; + policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) }); + + var exception = await Assert.ThrowsAsync<BadRequestException>( + () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); + Assert.Contains("User does not have two-step login enabled.", exception.Message); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_WithTwoFactorPolicyAndTwoFactorEnabled_Succeeds(Organization org, OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, + [OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy, + string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); + var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); + var userRepository = sutProvider.GetDependency<IUserRepository>(); + var policyService = sutProvider.GetDependency<IPolicyService>(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(); + + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; + orgUser.UserId = user.Id; + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); + organizationRepository.GetByIdAsync(org.Id).Returns(org); + userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); + twoFactorPolicy.OrganizationId = org.Id; + policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, true) }); + + await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id); + } + + [Theory, BitAutoData] + public async Task ConfirmUsersAsync_WithMultipleUsers_ReturnsExpectedMixedResults(Organization org, + OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser1, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser3, + OrganizationUser anotherOrgUser, User user1, User user2, User user3, + [OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy, + [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy, + string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); + var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); + var userRepository = sutProvider.GetDependency<IUserRepository>(); + var policyService = sutProvider.GetDependency<IPolicyService>(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(); + + org.PlanType = PlanType.EnterpriseAnnually; + orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = confirmingUser.OrganizationId = org.Id; + orgUser1.UserId = user1.Id; + orgUser2.UserId = user2.Id; + orgUser3.UserId = user3.Id; + anotherOrgUser.UserId = user3.Id; + var orgUsers = new[] { orgUser1, orgUser2, orgUser3 }; + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(orgUsers); + organizationRepository.GetByIdAsync(org.Id).Returns(org); + userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user1, user2, user3 }); + twoFactorPolicy.OrganizationId = org.Id; + policyService.GetPoliciesApplicableToUserAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id) && ids.Contains(user3.Id))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() + { + (user1.Id, true), + (user2.Id, false), + (user3.Id, true) + }); + singleOrgPolicy.OrganizationId = org.Id; + policyService.GetPoliciesApplicableToUserAsync(user3.Id, PolicyType.SingleOrg) + .Returns(new[] { singleOrgPolicy }); + organizationUserRepository.GetManyByManyUsersAsync(default) + .ReturnsForAnyArgs(new[] { orgUser1, orgUser2, orgUser3, anotherOrgUser }); + + var keys = orgUsers.ToDictionary(ou => ou.Id, _ => key); + var result = await sutProvider.Sut.ConfirmUsersAsync(confirmingUser.OrganizationId, keys, confirmingUser.Id); + Assert.Contains("", result[0].Item2); + Assert.Contains("User does not have two-step login enabled.", result[1].Item2); + Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2); + } +} diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index 4c42fdfeb9..82dc0e2ebe 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -24,7 +24,6 @@ using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Test.AdminConsole.AutoFixture; using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; using Bit.Core.Tokens; @@ -978,306 +977,6 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider) sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true); } - [Theory, BitAutoData] - public async Task ConfirmUser_InvalidStatus(OrganizationUser confirmingUser, - [OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser orgUser, string key, - SutProvider<OrganizationService> sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); - - organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser); - - var exception = await Assert.ThrowsAsync<BadRequestException>( - () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); - Assert.Contains("User not valid.", exception.Message); - } - - [Theory, BitAutoData] - public async Task ConfirmUser_WrongOrganization(OrganizationUser confirmingUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, string key, - SutProvider<OrganizationService> sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); - - organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser); - - var exception = await Assert.ThrowsAsync<BadRequestException>( - () => sutProvider.Sut.ConfirmUserAsync(confirmingUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); - Assert.Contains("User not valid.", exception.Message); - } - - [Theory] - [BitAutoData(OrganizationUserType.Admin)] - [BitAutoData(OrganizationUserType.Owner)] - public async Task ConfirmUserToFree_AlreadyFreeAdminOrOwner_Throws(OrganizationUserType userType, Organization org, OrganizationUser confirmingUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, - string key, SutProvider<OrganizationService> sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); - var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); - var userRepository = sutProvider.GetDependency<IUserRepository>(); - - org.PlanType = PlanType.Free; - orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; - orgUser.UserId = user.Id; - orgUser.Type = userType; - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); - organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(orgUser.UserId.Value).Returns(1); - organizationRepository.GetByIdAsync(org.Id).Returns(org); - userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); - - var exception = await Assert.ThrowsAsync<BadRequestException>( - () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); - Assert.Contains("User can only be an admin of one free organization.", exception.Message); - } - - [Theory] - [BitAutoData(PlanType.Custom, OrganizationUserType.Admin)] - [BitAutoData(PlanType.Custom, OrganizationUserType.Owner)] - [BitAutoData(PlanType.EnterpriseAnnually, OrganizationUserType.Admin)] - [BitAutoData(PlanType.EnterpriseAnnually, OrganizationUserType.Owner)] - [BitAutoData(PlanType.EnterpriseAnnually2020, OrganizationUserType.Admin)] - [BitAutoData(PlanType.EnterpriseAnnually2020, OrganizationUserType.Owner)] - [BitAutoData(PlanType.EnterpriseAnnually2019, OrganizationUserType.Admin)] - [BitAutoData(PlanType.EnterpriseAnnually2019, OrganizationUserType.Owner)] - [BitAutoData(PlanType.EnterpriseMonthly, OrganizationUserType.Admin)] - [BitAutoData(PlanType.EnterpriseMonthly, OrganizationUserType.Owner)] - [BitAutoData(PlanType.EnterpriseMonthly2020, OrganizationUserType.Admin)] - [BitAutoData(PlanType.EnterpriseMonthly2020, OrganizationUserType.Owner)] - [BitAutoData(PlanType.EnterpriseMonthly2019, OrganizationUserType.Admin)] - [BitAutoData(PlanType.EnterpriseMonthly2019, OrganizationUserType.Owner)] - [BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Admin)] - [BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Owner)] - [BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Admin)] - [BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Owner)] - [BitAutoData(PlanType.TeamsAnnually, OrganizationUserType.Admin)] - [BitAutoData(PlanType.TeamsAnnually, OrganizationUserType.Owner)] - [BitAutoData(PlanType.TeamsAnnually2020, OrganizationUserType.Admin)] - [BitAutoData(PlanType.TeamsAnnually2020, OrganizationUserType.Owner)] - [BitAutoData(PlanType.TeamsAnnually2019, OrganizationUserType.Admin)] - [BitAutoData(PlanType.TeamsAnnually2019, OrganizationUserType.Owner)] - [BitAutoData(PlanType.TeamsMonthly, OrganizationUserType.Admin)] - [BitAutoData(PlanType.TeamsMonthly, OrganizationUserType.Owner)] - [BitAutoData(PlanType.TeamsMonthly2020, OrganizationUserType.Admin)] - [BitAutoData(PlanType.TeamsMonthly2020, OrganizationUserType.Owner)] - [BitAutoData(PlanType.TeamsMonthly2019, OrganizationUserType.Admin)] - [BitAutoData(PlanType.TeamsMonthly2019, OrganizationUserType.Owner)] - public async Task ConfirmUserToNonFree_AlreadyFreeAdminOrOwner_DoesNotThrow(PlanType planType, OrganizationUserType orgUserType, Organization org, OrganizationUser confirmingUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, - string key, SutProvider<OrganizationService> sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); - var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); - var userRepository = sutProvider.GetDependency<IUserRepository>(); - - org.PlanType = planType; - orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; - orgUser.UserId = user.Id; - orgUser.Type = orgUserType; - orgUser.AccessSecretsManager = false; - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); - organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(orgUser.UserId.Value).Returns(1); - organizationRepository.GetByIdAsync(org.Id).Returns(org); - userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); - - await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id); - - await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); - await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email); - await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1)); - } - - - [Theory, BitAutoData] - public async Task ConfirmUser_AsUser_SingleOrgPolicy_AppliedFromConfirmingOrg_Throws(Organization org, OrganizationUser confirmingUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, - OrganizationUser orgUserAnotherOrg, [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy, - string key, SutProvider<OrganizationService> sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); - var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); - var userRepository = sutProvider.GetDependency<IUserRepository>(); - var policyService = sutProvider.GetDependency<IPolicyService>(); - - org.PlanType = PlanType.EnterpriseAnnually; - orgUser.Status = OrganizationUserStatusType.Accepted; - orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; - orgUser.UserId = orgUserAnotherOrg.UserId = user.Id; - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); - organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg }); - organizationRepository.GetByIdAsync(org.Id).Returns(org); - userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); - singleOrgPolicy.OrganizationId = org.Id; - policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy }); - - var exception = await Assert.ThrowsAsync<BadRequestException>( - () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); - Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", exception.Message); - } - - [Theory, BitAutoData] - public async Task ConfirmUser_AsUser_SingleOrgPolicy_AppliedFromOtherOrg_Throws(Organization org, OrganizationUser confirmingUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, - OrganizationUser orgUserAnotherOrg, [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy, - string key, SutProvider<OrganizationService> sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); - var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); - var userRepository = sutProvider.GetDependency<IUserRepository>(); - var policyService = sutProvider.GetDependency<IPolicyService>(); - - org.PlanType = PlanType.EnterpriseAnnually; - orgUser.Status = OrganizationUserStatusType.Accepted; - orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; - orgUser.UserId = orgUserAnotherOrg.UserId = user.Id; - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); - organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg }); - organizationRepository.GetByIdAsync(org.Id).Returns(org); - userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); - singleOrgPolicy.OrganizationId = orgUserAnotherOrg.Id; - policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy }); - - var exception = await Assert.ThrowsAsync<BadRequestException>( - () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); - Assert.Contains("Cannot confirm this member to the organization because they are in another organization which forbids it.", exception.Message); - } - - [Theory] - [BitAutoData(OrganizationUserType.Admin)] - [BitAutoData(OrganizationUserType.Owner)] - public async Task ConfirmUser_AsOwnerOrAdmin_SingleOrgPolicy_ExcludedViaUserType_Success( - OrganizationUserType userType, Organization org, OrganizationUser confirmingUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, - OrganizationUser orgUserAnotherOrg, - string key, SutProvider<OrganizationService> sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); - var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); - var userRepository = sutProvider.GetDependency<IUserRepository>(); - - org.PlanType = PlanType.EnterpriseAnnually; - orgUser.Type = userType; - orgUser.Status = OrganizationUserStatusType.Accepted; - orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; - orgUser.UserId = orgUserAnotherOrg.UserId = user.Id; - orgUser.AccessSecretsManager = true; - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); - organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg }); - organizationRepository.GetByIdAsync(org.Id).Returns(org); - userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); - - await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id); - - await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); - await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, true); - await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1)); - } - - [Theory, BitAutoData] - public async Task ConfirmUser_TwoFactorPolicy_NotEnabled_Throws(Organization org, OrganizationUser confirmingUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, - OrganizationUser orgUserAnotherOrg, - [OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy, - string key, SutProvider<OrganizationService> sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); - var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); - var userRepository = sutProvider.GetDependency<IUserRepository>(); - var policyService = sutProvider.GetDependency<IPolicyService>(); - var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(); - - org.PlanType = PlanType.EnterpriseAnnually; - orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; - orgUser.UserId = orgUserAnotherOrg.UserId = user.Id; - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); - organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg }); - organizationRepository.GetByIdAsync(org.Id).Returns(org); - userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); - twoFactorPolicy.OrganizationId = org.Id; - policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); - twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id))) - .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) }); - - var exception = await Assert.ThrowsAsync<BadRequestException>( - () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); - Assert.Contains("User does not have two-step login enabled.", exception.Message); - } - - [Theory, BitAutoData] - public async Task ConfirmUser_TwoFactorPolicy_Enabled_Success(Organization org, OrganizationUser confirmingUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, - [OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy, - string key, SutProvider<OrganizationService> sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); - var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); - var userRepository = sutProvider.GetDependency<IUserRepository>(); - var policyService = sutProvider.GetDependency<IPolicyService>(); - var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(); - - org.PlanType = PlanType.EnterpriseAnnually; - orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; - orgUser.UserId = user.Id; - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); - organizationRepository.GetByIdAsync(org.Id).Returns(org); - userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); - twoFactorPolicy.OrganizationId = org.Id; - policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); - twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id))) - .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, true) }); - - await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id); - } - - [Theory, BitAutoData] - public async Task ConfirmUsers_Success(Organization org, - OrganizationUser confirmingUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser1, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser3, - OrganizationUser anotherOrgUser, User user1, User user2, User user3, - [OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy, - [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy, - string key, SutProvider<OrganizationService> sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>(); - var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>(); - var userRepository = sutProvider.GetDependency<IUserRepository>(); - var policyService = sutProvider.GetDependency<IPolicyService>(); - var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(); - - org.PlanType = PlanType.EnterpriseAnnually; - orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = confirmingUser.OrganizationId = org.Id; - orgUser1.UserId = user1.Id; - orgUser2.UserId = user2.Id; - orgUser3.UserId = user3.Id; - anotherOrgUser.UserId = user3.Id; - var orgUsers = new[] { orgUser1, orgUser2, orgUser3 }; - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(orgUsers); - organizationRepository.GetByIdAsync(org.Id).Returns(org); - userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user1, user2, user3 }); - twoFactorPolicy.OrganizationId = org.Id; - policyService.GetPoliciesApplicableToUserAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); - twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id) && ids.Contains(user3.Id))) - .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() - { - (user1.Id, true), - (user2.Id, false), - (user3.Id, true) - }); - singleOrgPolicy.OrganizationId = org.Id; - policyService.GetPoliciesApplicableToUserAsync(user3.Id, PolicyType.SingleOrg) - .Returns(new[] { singleOrgPolicy }); - organizationUserRepository.GetManyByManyUsersAsync(default) - .ReturnsForAnyArgs(new[] { orgUser1, orgUser2, orgUser3, anotherOrgUser }); - - var keys = orgUsers.ToDictionary(ou => ou.Id, _ => key); - var result = await sutProvider.Sut.ConfirmUsersAsync(confirmingUser.OrganizationId, keys, confirmingUser.Id); - Assert.Contains("", result[0].Item2); - Assert.Contains("User does not have two-step login enabled.", result[1].Item2); - Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2); - } - [Theory, BitAutoData] public async Task UpdateOrganizationKeysAsync_WithoutManageResetPassword_Throws(Guid orgId, string publicKey, string privateKey, SutProvider<OrganizationService> sutProvider) From efd33c3301e0c526172ef8212e6a94edf4b9c887 Mon Sep 17 00:00:00 2001 From: Addison Beck <addison@bitwarden.com> Date: Mon, 24 Mar 2025 13:33:51 -0400 Subject: [PATCH 11/19] chore: set correct version for upcoming scheduled release (#5550) --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index b9b8cf557a..2ede6ad8d1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ <PropertyGroup> <TargetFramework>net8.0</TargetFramework> - <Version>2025.3.6</Version> + <Version>2025.3.3</Version> <RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace> <ImplicitUsings>enable</ImplicitUsings> From f1a9545a0082e00cf6f3745543fa7c3b6ca00678 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 24 Mar 2025 13:48:20 -0400 Subject: [PATCH 12/19] Remove unneeded exclusions (#5478) --- src/Billing/Billing.csproj | 2 -- src/Core/Core.csproj | 2 +- src/Infrastructure.Dapper/Infrastructure.Dapper.csproj | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index 50e372791f..f32eccfe8c 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -3,8 +3,6 @@ <PropertyGroup> <UserSecretsId>bitwarden-Billing</UserSecretsId> <MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish> - <!-- Temp exclusions until warnings are fixed --> - <WarningsNotAsErrors>$(WarningsNotAsErrors);CS9113</WarningsNotAsErrors> </PropertyGroup> <PropertyGroup Condition=" '$(RunConfiguration)' == 'Billing' " /> diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 2a3edcdc00..ea72f3c785 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -4,7 +4,7 @@ <GenerateUserSecretsAttribute>false</GenerateUserSecretsAttribute> <DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile> <!-- Temp exclusions until warnings are fixed --> - <WarningsNotAsErrors>$(WarningsNotAsErrors);CS1570;CS1574;CS8602;CS9113;CS1998;CS8604</WarningsNotAsErrors> + <WarningsNotAsErrors>$(WarningsNotAsErrors);CS1570;CS1574;CS9113;CS1998</WarningsNotAsErrors> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> diff --git a/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj b/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj index 19512670ce..c51af39824 100644 --- a/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj +++ b/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <!-- Temp exclusions until warnings are fixed --> - <WarningsNotAsErrors>$(WarningsNotAsErrors);CS8618;CS4014</WarningsNotAsErrors> + <WarningsNotAsErrors>$(WarningsNotAsErrors);CS8618</WarningsNotAsErrors> </PropertyGroup> <ItemGroup> From 229aecb55cb7ebf5450feb2d0459266384405e1a Mon Sep 17 00:00:00 2001 From: Matt Andreko <mandreko@bitwarden.com> Date: Mon, 24 Mar 2025 14:20:42 -0400 Subject: [PATCH 13/19] Update SARIF upload to use proper branch (#5534) --- .github/workflows/build.yml | 2 ++ .github/workflows/scan.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3b96eeb468..8f125b7811 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -317,6 +317,8 @@ jobs: uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 with: sarif_file: ${{ steps.container-scan.outputs.sarif }} + sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }} + ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} upload: name: Upload diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 1fa5c9587c..fe88782e35 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -49,6 +49,8 @@ jobs: uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 with: sarif_file: cx_result.sarif + sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }} + ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} quality: name: Quality scan From 55980e8038daadca5a2831ac7d086e742c393fef Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann <mail@quexten.com> Date: Tue, 25 Mar 2025 15:23:01 +0100 Subject: [PATCH 14/19] [PM-16603] Add userkey rotation v2 (#5204) * Implement userkey rotation v2 * Update request models * Cleanup * Update tests * Improve test * Add tests * Fix formatting * Fix test * Remove whitespace * Fix namespace * Enable nullable on models * Fix build * Add tests and enable nullable on masterpasswordunlockdatamodel * Fix test * Remove rollback * Add tests * Make masterpassword hint optional * Update user query * Add EF test * Improve test * Cleanup * Set masterpassword hint * Remove connection close * Add tests for invalid kdf types * Update test/Core.Test/KeyManagement/UserKey/RotateUserAccountKeysCommandTests.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Fix formatting * Update src/Api/KeyManagement/Models/Requests/RotateAccountKeysAndDataRequestModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/KeyManagement/Models/Requests/AccountKeysRequestModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Fix imports * Fix tests * Remove null check * Add rollback --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --- .../Auth/Controllers/AccountsController.cs | 1 + .../Accounts/MasterPasswordUnlockDataModel.cs | 66 +++++++++ .../AccountsKeyManagementController.cs | 83 ++++++++++- .../Requests/AccountKeysRequestModel.cs | 10 ++ .../RotateAccountKeysAndDataRequestModel.cs | 13 ++ .../Models/Requests/UnlockDataRequestModel.cs | 16 +++ .../Models/Requests/UserDataRequestModel.cs | 12 ++ .../UserServiceCollectionExtensions.cs | 1 + src/Core/Constants.cs | 3 + .../Models/Data/MasterPasswordUnlockData.cs | 34 +++++ .../Models/Data/RotateUserAccountKeysData.cs | 28 ++++ .../UserKey/IRotateUserAccountKeysCommand.cs | 20 +++ .../RotateUserAccountkeysCommand.cs | 134 ++++++++++++++++++ src/Core/Repositories/IUserRepository.cs | 2 + .../Repositories/UserRepository.cs | 55 ++++++- .../Repositories/UserRepository.cs | 47 ++++++ .../AccountsKeyManagementControllerTests.cs | 91 ++++++++++++ .../AccountsKeyManagementControllerTests.cs | 85 +++++++++++ .../Request/MasterPasswordUnlockDataModel.cs | 68 +++++++++ .../RotateUserAccountKeysCommandTests.cs | 120 ++++++++++++++++ .../UserKey/RotateUserKeyCommandTests.cs | 2 +- .../Repositories/UserRepositoryTests.cs | 24 ++++ 22 files changed, 906 insertions(+), 9 deletions(-) create mode 100644 src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataModel.cs create mode 100644 src/Api/KeyManagement/Models/Requests/AccountKeysRequestModel.cs create mode 100644 src/Api/KeyManagement/Models/Requests/RotateAccountKeysAndDataRequestModel.cs create mode 100644 src/Api/KeyManagement/Models/Requests/UnlockDataRequestModel.cs create mode 100644 src/Api/KeyManagement/Models/Requests/UserDataRequestModel.cs create mode 100644 src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs create mode 100644 src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs create mode 100644 src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs create mode 100644 src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs create mode 100644 test/Api.Test/KeyManagement/Models/Request/MasterPasswordUnlockDataModel.cs create mode 100644 test/Core.Test/KeyManagement/UserKey/RotateUserAccountKeysCommandTests.cs diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 6c19049c49..2555a6fe2d 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -355,6 +355,7 @@ 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) { diff --git a/src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataModel.cs b/src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataModel.cs new file mode 100644 index 0000000000..ba57788cec --- /dev/null +++ b/src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataModel.cs @@ -0,0 +1,66 @@ +#nullable enable + +using System.ComponentModel.DataAnnotations; +using Bit.Core.Enums; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Utilities; + +namespace Bit.Api.Auth.Models.Request.Accounts; + +public class MasterPasswordUnlockDataModel : IValidatableObject +{ + public required KdfType KdfType { get; set; } + public required int KdfIterations { get; set; } + public int? KdfMemory { get; set; } + public int? KdfParallelism { get; set; } + + [StrictEmailAddress] + [StringLength(256)] + public required string Email { get; set; } + [StringLength(300)] + public required string MasterKeyAuthenticationHash { get; set; } + [EncryptedString] public required string MasterKeyEncryptedUserKey { get; set; } + [StringLength(50)] + public string? MasterPasswordHint { get; set; } + + public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) + { + if (KdfType == KdfType.PBKDF2_SHA256) + { + if (KdfMemory.HasValue || KdfParallelism.HasValue) + { + yield return new ValidationResult("KdfMemory and KdfParallelism must be null for PBKDF2_SHA256", new[] { nameof(KdfMemory), nameof(KdfParallelism) }); + } + } + else if (KdfType == KdfType.Argon2id) + { + if (!KdfMemory.HasValue || !KdfParallelism.HasValue) + { + yield return new ValidationResult("KdfMemory and KdfParallelism must have values for Argon2id", new[] { nameof(KdfMemory), nameof(KdfParallelism) }); + } + } + else + { + yield return new ValidationResult("Invalid KdfType", new[] { nameof(KdfType) }); + } + } + + public MasterPasswordUnlockData ToUnlockData() + { + var data = new MasterPasswordUnlockData + { + KdfType = KdfType, + KdfIterations = KdfIterations, + KdfMemory = KdfMemory, + KdfParallelism = KdfParallelism, + + Email = Email, + + MasterKeyAuthenticationHash = MasterKeyAuthenticationHash, + MasterKeyEncryptedUserKey = MasterKeyEncryptedUserKey, + MasterPasswordHint = MasterPasswordHint + }; + return data; + } + +} diff --git a/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs index b8d5e30949..85e0981f22 100644 --- a/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs +++ b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs @@ -1,10 +1,23 @@ #nullable enable +using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Api.Auth.Models.Request; +using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.KeyManagement.Models.Requests; +using Bit.Api.KeyManagement.Validators; +using Bit.Api.Tools.Models.Request; +using Bit.Api.Vault.Models.Request; using Bit.Core; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.KeyManagement.Commands.Interfaces; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Tools.Entities; +using Bit.Core.Vault.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -19,18 +32,45 @@ public class AccountsKeyManagementController : Controller private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IRegenerateUserAsymmetricKeysCommand _regenerateUserAsymmetricKeysCommand; private readonly IUserService _userService; + private readonly IRotateUserAccountKeysCommand _rotateUserAccountKeysCommand; + private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator; + private readonly IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> _folderValidator; + private readonly IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> _sendValidator; + private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>> + _emergencyAccessValidator; + private readonly IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, + IReadOnlyList<OrganizationUser>> + _organizationUserValidator; + private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> + _webauthnKeyValidator; public AccountsKeyManagementController(IUserService userService, IFeatureService featureService, IOrganizationUserRepository organizationUserRepository, IEmergencyAccessRepository emergencyAccessRepository, - IRegenerateUserAsymmetricKeysCommand regenerateUserAsymmetricKeysCommand) + IRegenerateUserAsymmetricKeysCommand regenerateUserAsymmetricKeysCommand, + IRotateUserAccountKeysCommand rotateUserKeyCommandV2, + IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator, + IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator, + IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> sendValidator, + IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>> + emergencyAccessValidator, + IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>> + organizationUserValidator, + IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator) { _userService = userService; _featureService = featureService; _regenerateUserAsymmetricKeysCommand = regenerateUserAsymmetricKeysCommand; _organizationUserRepository = organizationUserRepository; _emergencyAccessRepository = emergencyAccessRepository; + _rotateUserAccountKeysCommand = rotateUserKeyCommandV2; + _cipherValidator = cipherValidator; + _folderValidator = folderValidator; + _sendValidator = sendValidator; + _emergencyAccessValidator = emergencyAccessValidator; + _organizationUserValidator = organizationUserValidator; + _webauthnKeyValidator = webAuthnKeyValidator; } [HttpPost("regenerate-keys")] @@ -47,4 +87,45 @@ public class AccountsKeyManagementController : Controller await _regenerateUserAsymmetricKeysCommand.RegenerateKeysAsync(request.ToUserAsymmetricKeys(user.Id), usersOrganizationAccounts, designatedEmergencyAccess); } + + + [HttpPost("rotate-user-account-keys")] + public async Task RotateUserAccountKeysAsync([FromBody] RotateUserAccountKeysAndDataRequestModel model) + { + var user = await _userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + var dataModel = new RotateUserAccountKeysData + { + OldMasterKeyAuthenticationHash = model.OldMasterKeyAuthenticationHash, + + UserKeyEncryptedAccountPrivateKey = model.AccountKeys.UserKeyEncryptedAccountPrivateKey, + AccountPublicKey = model.AccountKeys.AccountPublicKey, + + MasterPasswordUnlockData = model.AccountUnlockData.MasterPasswordUnlockData.ToUnlockData(), + EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.AccountUnlockData.EmergencyAccessUnlockData), + OrganizationUsers = await _organizationUserValidator.ValidateAsync(user, model.AccountUnlockData.OrganizationAccountRecoveryUnlockData), + WebAuthnKeys = await _webauthnKeyValidator.ValidateAsync(user, model.AccountUnlockData.PasskeyUnlockData), + + Ciphers = await _cipherValidator.ValidateAsync(user, model.AccountData.Ciphers), + Folders = await _folderValidator.ValidateAsync(user, model.AccountData.Folders), + Sends = await _sendValidator.ValidateAsync(user, model.AccountData.Sends), + }; + + var result = await _rotateUserAccountKeysCommand.RotateUserAccountKeysAsync(user, dataModel); + if (result.Succeeded) + { + return; + } + + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + + throw new BadRequestException(ModelState); + } } diff --git a/src/Api/KeyManagement/Models/Requests/AccountKeysRequestModel.cs b/src/Api/KeyManagement/Models/Requests/AccountKeysRequestModel.cs new file mode 100644 index 0000000000..7c7de4d210 --- /dev/null +++ b/src/Api/KeyManagement/Models/Requests/AccountKeysRequestModel.cs @@ -0,0 +1,10 @@ +#nullable enable +using Bit.Core.Utilities; + +namespace Bit.Api.KeyManagement.Models.Requests; + +public class AccountKeysRequestModel +{ + [EncryptedString] public required string UserKeyEncryptedAccountPrivateKey { get; set; } + public required string AccountPublicKey { get; set; } +} diff --git a/src/Api/KeyManagement/Models/Requests/RotateAccountKeysAndDataRequestModel.cs b/src/Api/KeyManagement/Models/Requests/RotateAccountKeysAndDataRequestModel.cs new file mode 100644 index 0000000000..b0b19e2bd3 --- /dev/null +++ b/src/Api/KeyManagement/Models/Requests/RotateAccountKeysAndDataRequestModel.cs @@ -0,0 +1,13 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.KeyManagement.Models.Requests; + +public class RotateUserAccountKeysAndDataRequestModel +{ + [StringLength(300)] + public required string OldMasterKeyAuthenticationHash { get; set; } + public required UnlockDataRequestModel AccountUnlockData { get; set; } + public required AccountKeysRequestModel AccountKeys { get; set; } + public required AccountDataRequestModel AccountData { get; set; } +} diff --git a/src/Api/KeyManagement/Models/Requests/UnlockDataRequestModel.cs b/src/Api/KeyManagement/Models/Requests/UnlockDataRequestModel.cs new file mode 100644 index 0000000000..5156e2a655 --- /dev/null +++ b/src/Api/KeyManagement/Models/Requests/UnlockDataRequestModel.cs @@ -0,0 +1,16 @@ +#nullable enable +using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Api.Auth.Models.Request; +using Bit.Api.Auth.Models.Request.Accounts; +using Bit.Api.Auth.Models.Request.WebAuthn; + +namespace Bit.Api.KeyManagement.Models.Requests; + +public class UnlockDataRequestModel +{ + // All methods to get to the userkey + public required MasterPasswordUnlockDataModel MasterPasswordUnlockData { get; set; } + public required IEnumerable<EmergencyAccessWithIdRequestModel> EmergencyAccessUnlockData { get; set; } + public required IEnumerable<ResetPasswordWithOrgIdRequestModel> OrganizationAccountRecoveryUnlockData { get; set; } + public required IEnumerable<WebAuthnLoginRotateKeyRequestModel> PasskeyUnlockData { get; set; } +} diff --git a/src/Api/KeyManagement/Models/Requests/UserDataRequestModel.cs b/src/Api/KeyManagement/Models/Requests/UserDataRequestModel.cs new file mode 100644 index 0000000000..f854d82bcc --- /dev/null +++ b/src/Api/KeyManagement/Models/Requests/UserDataRequestModel.cs @@ -0,0 +1,12 @@ +#nullable enable +using Bit.Api.Tools.Models.Request; +using Bit.Api.Vault.Models.Request; + +namespace Bit.Api.KeyManagement.Models.Requests; + +public class AccountDataRequestModel +{ + public required IEnumerable<CipherWithIdRequestModel> Ciphers { get; set; } + public required IEnumerable<FolderWithIdRequestModel> Folders { get; set; } + public required IEnumerable<SendWithIdRequestModel> Sends { get; set; } +} diff --git a/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs b/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs index df102c855f..16a0ef9805 100644 --- a/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs +++ b/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs @@ -32,6 +32,7 @@ public static class UserServiceCollectionExtensions public static void AddUserKeyCommands(this IServiceCollection services, IGlobalSettings globalSettings) { services.AddScoped<IRotateUserKeyCommand, RotateUserKeyCommand>(); + services.AddScoped<IRotateUserAccountKeysCommand, RotateUserAccountKeysCommand>(); } private static void AddUserPasswordCommands(this IServiceCollection services) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 0ae9f1d8d7..e41391b173 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -168,6 +168,9 @@ public static class FeatureFlagKeys public const string Argon2Default = "argon2-default"; public const string UsePricingService = "use-pricing-service"; public const string RecordInstallationLastActivityDate = "installation-last-activity-date"; + public const string UserkeyRotationV2 = "userkey-rotation-v2"; + public const string EnablePasswordManagerSyncAndroid = "enable-password-manager-sync-android"; + public const string EnablePasswordManagerSynciOS = "enable-password-manager-sync-ios"; public const string AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner"; public const string SingleTapPasskeyCreation = "single-tap-passkey-creation"; public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication"; diff --git a/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs b/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs new file mode 100644 index 0000000000..0ddfc03190 --- /dev/null +++ b/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs @@ -0,0 +1,34 @@ +#nullable enable +using Bit.Core.Entities; +using Bit.Core.Enums; + +namespace Bit.Core.KeyManagement.Models.Data; + +public class MasterPasswordUnlockData +{ + public KdfType KdfType { get; set; } + public int KdfIterations { get; set; } + public int? KdfMemory { get; set; } + public int? KdfParallelism { get; set; } + + public required string Email { get; set; } + public required string MasterKeyAuthenticationHash { get; set; } + public required string MasterKeyEncryptedUserKey { get; set; } + public string? MasterPasswordHint { get; set; } + + public bool ValidateForUser(User user) + { + if (KdfType != user.Kdf || KdfMemory != user.KdfMemory || KdfParallelism != user.KdfParallelism || KdfIterations != user.KdfIterations) + { + return false; + } + else if (Email != user.Email) + { + return false; + } + else + { + return true; + } + } +} diff --git a/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs b/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs new file mode 100644 index 0000000000..7cb1c273a3 --- /dev/null +++ b/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs @@ -0,0 +1,28 @@ +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 RotateUserAccountKeysData +{ + // Authentication for this requests + public string OldMasterKeyAuthenticationHash { get; set; } + + // Other keys encrypted by the userkey + public string UserKeyEncryptedAccountPrivateKey { get; set; } + public string AccountPublicKey { get; set; } + + // All methods to get to the userkey + public MasterPasswordUnlockData MasterPasswordUnlockData { get; set; } + public IEnumerable<EmergencyAccess> EmergencyAccesses { get; set; } + public IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; } + public IEnumerable<WebAuthnLoginRotateKeyData> WebAuthnKeys { get; set; } + + // User vault data encrypted by the userkey + public IEnumerable<Cipher> Ciphers { get; set; } + public IEnumerable<Folder> Folders { get; set; } + public IReadOnlyList<Send> Sends { get; set; } +} diff --git a/src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs b/src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs new file mode 100644 index 0000000000..ec40e7031d --- /dev/null +++ b/src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs @@ -0,0 +1,20 @@ +using Bit.Core.Entities; +using Bit.Core.KeyManagement.Models.Data; +using Microsoft.AspNetCore.Identity; + +namespace Bit.Core.KeyManagement.UserKey; + +/// <summary> +/// Responsible for rotation of a user key and updating database with re-encrypted data +/// </summary> +public interface IRotateUserAccountKeysCommand +{ + /// <summary> + /// Sets a new user key and updates all encrypted data. + /// </summary> + /// <param name="model">All necessary information for rotation. If data is not included, this will lead to the change being rejected.</param> + /// <returns>An IdentityResult for verification of the master password hash</returns> + /// <exception cref="ArgumentNullException">User must be provided.</exception> + /// <exception cref="InvalidOperationException">User KDF settings and email must match the model provided settings.</exception> + Task<IdentityResult> RotateUserAccountKeysAsync(User user, RotateUserAccountKeysData model); +} diff --git a/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs b/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs new file mode 100644 index 0000000000..f4dcf31d5c --- /dev/null +++ b/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs @@ -0,0 +1,134 @@ +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; + +/// <inheritdoc /> +public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand +{ + 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; + private readonly IPasswordHasher<User> _passwordHasher; + + /// <summary> + /// Instantiates a new <see cref="RotateUserAccountKeysCommand"/> + /// </summary> + /// <param name="userService">Master password hash validation</param> + /// <param name="userRepository">Updates user keys and re-encrypted data if needed</param> + /// <param name="cipherRepository">Provides a method to update re-encrypted cipher data</param> + /// <param name="folderRepository">Provides a method to update re-encrypted folder data</param> + /// <param name="sendRepository">Provides a method to update re-encrypted send data</param> + /// <param name="emergencyAccessRepository">Provides a method to update re-encrypted emergency access data</param> + /// <param name="organizationUserRepository">Provides a method to update re-encrypted organization user data</param> + /// <param name="passwordHasher">Hashes the new master password</param> + /// <param name="pushService">Logs out user from other devices after successful rotation</param> + /// <param name="errors">Provides a password mismatch error if master password hash validation fails</param> + /// <param name="credentialRepository">Provides a method to update re-encrypted WebAuthn keys</param> + public RotateUserAccountKeysCommand(IUserService userService, IUserRepository userRepository, + ICipherRepository cipherRepository, IFolderRepository folderRepository, ISendRepository sendRepository, + IEmergencyAccessRepository emergencyAccessRepository, IOrganizationUserRepository organizationUserRepository, + IPasswordHasher<User> passwordHasher, + 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; + _passwordHasher = passwordHasher; + } + + /// <inheritdoc /> + public async Task<IdentityResult> RotateUserAccountKeysAsync(User user, RotateUserAccountKeysData model) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (!await _userService.CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)) + { + return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()); + } + + var now = DateTime.UtcNow; + user.RevisionDate = user.AccountRevisionDate = now; + user.LastKeyRotationDate = now; + user.SecurityStamp = Guid.NewGuid().ToString(); + + if ( + !model.MasterPasswordUnlockData.ValidateForUser(user) + ) + { + throw new InvalidOperationException("The provided master password unlock data is not valid for this user."); + } + if ( + model.AccountPublicKey != user.PublicKey + ) + { + throw new InvalidOperationException("The provided account public key does not match the user's current public key, and changing the account asymmetric keypair is currently not supported during key rotation."); + } + + user.Key = model.MasterPasswordUnlockData.MasterKeyEncryptedUserKey; + user.PrivateKey = model.UserKeyEncryptedAccountPrivateKey; + user.MasterPassword = _passwordHasher.HashPassword(user, model.MasterPasswordUnlockData.MasterKeyAuthenticationHash); + user.MasterPasswordHint = model.MasterPasswordUnlockData.MasterPasswordHint; + + List<UpdateEncryptedDataForKeyRotation> 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.UpdateUserKeyAndEncryptedDataV2Async(user, saveEncryptedDataActions); + await _pushService.PushLogOutAsync(user.Id); + return IdentityResult.Success; + } +} diff --git a/src/Core/Repositories/IUserRepository.cs b/src/Core/Repositories/IUserRepository.cs index 040e6e1f49..0e59b9998f 100644 --- a/src/Core/Repositories/IUserRepository.cs +++ b/src/Core/Repositories/IUserRepository.cs @@ -32,5 +32,7 @@ public interface IUserRepository : IRepository<User, Guid> /// <param name="updateDataActions">Registered database calls to update re-encrypted data.</param> Task UpdateUserKeyAndEncryptedDataAsync(User user, IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions); + Task UpdateUserKeyAndEncryptedDataV2Async(User user, + IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions); Task DeleteManyAsync(IEnumerable<User> users); } diff --git a/src/Infrastructure.Dapper/Repositories/UserRepository.cs b/src/Infrastructure.Dapper/Repositories/UserRepository.cs index 227a7c03e5..28478a0c41 100644 --- a/src/Infrastructure.Dapper/Repositories/UserRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/UserRepository.cs @@ -254,6 +254,42 @@ public class UserRepository : Repository<User, Guid>, IUserRepository } + public async Task UpdateUserKeyAndEncryptedDataV2Async( + User user, + IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions) + { + await using var connection = new SqlConnection(ConnectionString); + connection.Open(); + + await using var transaction = connection.BeginTransaction(); + try + { + user.AccountRevisionDate = user.RevisionDate; + + ProtectData(user); + await connection.ExecuteAsync( + $"[{Schema}].[{Table}_Update]", + user, + transaction: transaction, + commandType: CommandType.StoredProcedure); + + // Update re-encrypted data + foreach (var action in updateDataActions) + { + await action(connection, transaction); + } + transaction.Commit(); + } + catch + { + transaction.Rollback(); + UnprotectData(user); + throw; + } + UnprotectData(user); + } + + public async Task<IEnumerable<User>> GetManyAsync(IEnumerable<Guid> ids) { using (var connection = new SqlConnection(ReadOnlyConnectionString)) @@ -295,6 +331,18 @@ public class UserRepository : Repository<User, Guid>, IUserRepository var originalKey = user.Key; // Protect values + ProtectData(user); + + // Save + await saveTask(); + + // Restore original values + user.MasterPassword = originalMasterPassword; + user.Key = originalKey; + } + + private void ProtectData(User user) + { if (!user.MasterPassword?.StartsWith(Constants.DatabaseFieldProtectedPrefix) ?? false) { user.MasterPassword = string.Concat(Constants.DatabaseFieldProtectedPrefix, @@ -306,13 +354,6 @@ public class UserRepository : Repository<User, Guid>, IUserRepository user.Key = string.Concat(Constants.DatabaseFieldProtectedPrefix, _dataProtector.Protect(user.Key!)); } - - // Save - await saveTask(); - - // Restore original values - user.MasterPassword = originalMasterPassword; - user.Key = originalKey; } private void UnprotectData(User? user) diff --git a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs index cbfefb6483..127646ed59 100644 --- a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs @@ -170,6 +170,7 @@ public class UserRepository : Repository<Core.Entities.User, User, Guid>, IUserR entity.SecurityStamp = user.SecurityStamp; entity.Key = user.Key; + entity.PrivateKey = user.PrivateKey; entity.LastKeyRotationDate = user.LastKeyRotationDate; entity.AccountRevisionDate = user.AccountRevisionDate; @@ -194,6 +195,52 @@ public class UserRepository : Repository<Core.Entities.User, User, Guid>, IUserR } + + public async Task UpdateUserKeyAndEncryptedDataV2Async(Core.Entities.User user, + IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + await using var transaction = await dbContext.Database.BeginTransactionAsync(); + + // Update user + var userEntity = await dbContext.Users.FindAsync(user.Id); + if (userEntity == null) + { + throw new ArgumentException("User not found", nameof(user)); + } + + userEntity.SecurityStamp = user.SecurityStamp; + userEntity.Key = user.Key; + userEntity.PrivateKey = user.PrivateKey; + + userEntity.Kdf = user.Kdf; + userEntity.KdfIterations = user.KdfIterations; + userEntity.KdfMemory = user.KdfMemory; + userEntity.KdfParallelism = user.KdfParallelism; + + userEntity.Email = user.Email; + + userEntity.MasterPassword = user.MasterPassword; + userEntity.MasterPasswordHint = user.MasterPasswordHint; + + userEntity.LastKeyRotationDate = user.LastKeyRotationDate; + userEntity.AccountRevisionDate = user.AccountRevisionDate; + userEntity.RevisionDate = user.RevisionDate; + + await dbContext.SaveChangesAsync(); + + // Update re-encrypted data + foreach (var action in updateDataActions) + { + // connection and transaction aren't used in EF + await action(); + } + + await transaction.CommitAsync(); + } + public async Task<IEnumerable<Core.Entities.User>> GetManyAsync(IEnumerable<Guid> ids) { using (var scope = ServiceScopeFactory.CreateScope()) diff --git a/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs b/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs index ec7ca37460..7c05e1d680 100644 --- a/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs +++ b/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -2,12 +2,18 @@ using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.Helpers; using Bit.Api.KeyManagement.Models.Requests; +using Bit.Api.Tools.Models.Request; +using Bit.Api.Vault.Models; +using Bit.Api.Vault.Models.Request; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Billing.Enums; +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; +using Bit.Core.Vault.Enums; using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Identity; using Xunit; namespace Bit.Api.IntegrationTest.KeyManagement.Controllers; @@ -23,6 +29,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication private readonly ApiApplicationFactory _factory; private readonly LoginHelper _loginHelper; private readonly IUserRepository _userRepository; + private readonly IPasswordHasher<User> _passwordHasher; private string _ownerEmail = null!; public AccountsKeyManagementControllerTests(ApiApplicationFactory factory) @@ -35,6 +42,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication _userRepository = _factory.GetService<IUserRepository>(); _emergencyAccessRepository = _factory.GetService<IEmergencyAccessRepository>(); _organizationUserRepository = _factory.GetService<IOrganizationUserRepository>(); + _passwordHasher = _factory.GetService<IPasswordHasher<User>>(); } public async Task InitializeAsync() @@ -161,4 +169,87 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication }; await _emergencyAccessRepository.CreateAsync(emergencyAccess); } + + [Theory] + [BitAutoData] + public async Task RotateUserAccountKeysAsync_NotLoggedIn_Unauthorized(RotateUserAccountKeysAndDataRequestModel request) + { + var response = await _client.PostAsJsonAsync("/accounts/key-management/rotate-user-account-keys", request); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Theory] + [BitAutoData] + public async Task RotateUserAccountKeysAsync_Success(RotateUserAccountKeysAndDataRequestModel request) + { + await _loginHelper.LoginAsync(_ownerEmail); + var user = await _userRepository.GetByEmailAsync(_ownerEmail); + if (user == null) + { + throw new InvalidOperationException("User not found."); + } + + var password = _passwordHasher.HashPassword(user, "newMasterPassword"); + user.MasterPassword = password; + user.PublicKey = "publicKey"; + await _userRepository.ReplaceAsync(user); + + request.AccountUnlockData.MasterPasswordUnlockData.KdfType = user.Kdf; + request.AccountUnlockData.MasterPasswordUnlockData.KdfIterations = user.KdfIterations; + request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory = user.KdfMemory; + request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism = user.KdfParallelism; + request.AccountUnlockData.MasterPasswordUnlockData.Email = user.Email; + request.AccountKeys.AccountPublicKey = "publicKey"; + request.AccountKeys.UserKeyEncryptedAccountPrivateKey = _mockEncryptedString; + + request.OldMasterKeyAuthenticationHash = "newMasterPassword"; + + request.AccountData.Ciphers = + [ + new CipherWithIdRequestModel + { + Id = Guid.NewGuid(), + Type = CipherType.Login, + Name = _mockEncryptedString, + Login = new CipherLoginModel + { + Username = _mockEncryptedString, + Password = _mockEncryptedString, + }, + }, + ]; + request.AccountData.Folders = [ + new FolderWithIdRequestModel + { + Id = Guid.NewGuid(), + Name = _mockEncryptedString, + }, + ]; + request.AccountData.Sends = [ + new SendWithIdRequestModel + { + Id = Guid.NewGuid(), + Name = _mockEncryptedString, + Key = _mockEncryptedString, + Disabled = false, + DeletionDate = DateTime.UtcNow.AddDays(1), + }, + ]; + request.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey = _mockEncryptedString; + request.AccountUnlockData.PasskeyUnlockData = []; + request.AccountUnlockData.EmergencyAccessUnlockData = []; + request.AccountUnlockData.OrganizationAccountRecoveryUnlockData = []; + + var response = await _client.PostAsJsonAsync("/accounts/key-management/rotate-user-account-keys", request); + response.EnsureSuccessStatusCode(); + + var userNewState = await _userRepository.GetByEmailAsync(_ownerEmail); + Assert.NotNull(userNewState); + Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.Email, userNewState.Email); + Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfType, userNewState.Kdf); + Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfIterations, userNewState.KdfIterations); + Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory, userNewState.KdfMemory); + Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism, userNewState.KdfParallelism); + } } diff --git a/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs index 2615697ad3..49c4f88cb4 100644 --- a/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs +++ b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -1,17 +1,28 @@ #nullable enable using System.Security.Claims; +using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Api.Auth.Models.Request; +using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.KeyManagement.Controllers; using Bit.Api.KeyManagement.Models.Requests; +using Bit.Api.KeyManagement.Validators; +using Bit.Api.Tools.Models.Request; +using Bit.Api.Vault.Models.Request; using Bit.Core; +using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Data; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.KeyManagement.Commands.Interfaces; using Bit.Core.KeyManagement.Models.Data; +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; using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Identity; using NSubstitute; using NSubstitute.ReturnsExtensions; using Xunit; @@ -93,4 +104,78 @@ public class AccountsKeyManagementControllerTests Arg.Is(orgUsers), Arg.Is(accessDetails)); } + + [Theory] + [BitAutoData] + public async Task RotateUserAccountKeysSuccess(SutProvider<AccountsKeyManagementController> sutProvider, + RotateUserAccountKeysAndDataRequestModel data, User user) + { + sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); + sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>()) + .Returns(IdentityResult.Success); + await sutProvider.Sut.RotateUserAccountKeysAsync(data); + + await sutProvider.GetDependency<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>>().Received(1) + .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.EmergencyAccessUnlockData)); + await sutProvider.GetDependency<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>>().Received(1) + .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.OrganizationAccountRecoveryUnlockData)); + await sutProvider.GetDependency<IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>>().Received(1) + .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.PasskeyUnlockData)); + + await sutProvider.GetDependency<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>>().Received(1) + .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Ciphers)); + await sutProvider.GetDependency<IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>>().Received(1) + .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Folders)); + await sutProvider.GetDependency<IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>>().Received(1) + .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Sends)); + + await sutProvider.GetDependency<IRotateUserAccountKeysCommand>().Received(1) + .RotateUserAccountKeysAsync(Arg.Is(user), Arg.Is<RotateUserAccountKeysData>(d => + d.OldMasterKeyAuthenticationHash == data.OldMasterKeyAuthenticationHash + + && d.MasterPasswordUnlockData.KdfType == data.AccountUnlockData.MasterPasswordUnlockData.KdfType + && d.MasterPasswordUnlockData.KdfIterations == data.AccountUnlockData.MasterPasswordUnlockData.KdfIterations + && d.MasterPasswordUnlockData.KdfMemory == data.AccountUnlockData.MasterPasswordUnlockData.KdfMemory + && d.MasterPasswordUnlockData.KdfParallelism == data.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism + && d.MasterPasswordUnlockData.Email == data.AccountUnlockData.MasterPasswordUnlockData.Email + + && d.MasterPasswordUnlockData.MasterKeyAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash + && d.MasterPasswordUnlockData.MasterKeyEncryptedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey + + && d.AccountPublicKey == data.AccountKeys.AccountPublicKey + && d.UserKeyEncryptedAccountPrivateKey == data.AccountKeys.UserKeyEncryptedAccountPrivateKey + )); + } + + + [Theory] + [BitAutoData] + public async Task RotateUserKeyNoUser_Throws(SutProvider<AccountsKeyManagementController> sutProvider, + RotateUserAccountKeysAndDataRequestModel data) + { + User? user = null; + sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); + sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>()) + .Returns(IdentityResult.Success); + await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.RotateUserAccountKeysAsync(data)); + } + + [Theory] + [BitAutoData] + public async Task RotateUserKeyWrongData_Throws(SutProvider<AccountsKeyManagementController> sutProvider, + RotateUserAccountKeysAndDataRequestModel data, User user, IdentityErrorDescriber _identityErrorDescriber) + { + sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); + sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>()) + .Returns(IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch())); + try + { + await sutProvider.Sut.RotateUserAccountKeysAsync(data); + Assert.Fail("Should have thrown"); + } + catch (BadRequestException ex) + { + Assert.NotEmpty(ex.ModelState.Values); + } + } } diff --git a/test/Api.Test/KeyManagement/Models/Request/MasterPasswordUnlockDataModel.cs b/test/Api.Test/KeyManagement/Models/Request/MasterPasswordUnlockDataModel.cs new file mode 100644 index 0000000000..4c78c7015a --- /dev/null +++ b/test/Api.Test/KeyManagement/Models/Request/MasterPasswordUnlockDataModel.cs @@ -0,0 +1,68 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using Bit.Api.Auth.Models.Request.Accounts; +using Bit.Core.Enums; +using Xunit; + +namespace Bit.Api.Test.KeyManagement.Models.Request; + +public class MasterPasswordUnlockDataModelTests +{ + + readonly string _mockEncryptedString = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; + + [Theory] + [InlineData(KdfType.PBKDF2_SHA256, 5000, null, null)] + [InlineData(KdfType.PBKDF2_SHA256, 100000, null, null)] + [InlineData(KdfType.PBKDF2_SHA256, 600000, null, null)] + [InlineData(KdfType.Argon2id, 3, 64, 4)] + public void Validate_Success(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism) + { + var model = new MasterPasswordUnlockDataModel + { + KdfType = kdfType, + KdfIterations = kdfIterations, + KdfMemory = kdfMemory, + KdfParallelism = kdfParallelism, + Email = "example@example.com", + MasterKeyAuthenticationHash = "hash", + MasterKeyEncryptedUserKey = _mockEncryptedString, + MasterPasswordHint = "hint" + }; + var result = Validate(model); + Assert.Empty(result); + } + + [Theory] + [InlineData(KdfType.Argon2id, 1, null, 1)] + [InlineData(KdfType.Argon2id, 1, 64, null)] + [InlineData(KdfType.PBKDF2_SHA256, 5000, 0, null)] + [InlineData(KdfType.PBKDF2_SHA256, 5000, null, 0)] + [InlineData(KdfType.PBKDF2_SHA256, 5000, 0, 0)] + [InlineData((KdfType)2, 100000, null, null)] + [InlineData((KdfType)2, 2, 64, 4)] + public void Validate_Failure(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism) + { + var model = new MasterPasswordUnlockDataModel + { + KdfType = kdfType, + KdfIterations = kdfIterations, + KdfMemory = kdfMemory, + KdfParallelism = kdfParallelism, + Email = "example@example.com", + MasterKeyAuthenticationHash = "hash", + MasterKeyEncryptedUserKey = _mockEncryptedString, + MasterPasswordHint = "hint" + }; + var result = Validate(model); + Assert.Single(result); + Assert.NotNull(result.First().ErrorMessage); + } + + private static List<ValidationResult> Validate(MasterPasswordUnlockDataModel model) + { + var results = new List<ValidationResult>(); + Validator.TryValidateObject(model, new ValidationContext(model), results, true); + return results; + } +} diff --git a/test/Core.Test/KeyManagement/UserKey/RotateUserAccountKeysCommandTests.cs b/test/Core.Test/KeyManagement/UserKey/RotateUserAccountKeysCommandTests.cs new file mode 100644 index 0000000000..e677814fc1 --- /dev/null +++ b/test/Core.Test/KeyManagement/UserKey/RotateUserAccountKeysCommandTests.cs @@ -0,0 +1,120 @@ +using Bit.Core.Entities; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.UserKey.Implementations; +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 RotateUserAccountKeysCommandTests +{ + [Theory, BitAutoData] + public async Task RejectsWrongOldMasterPassword(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, + RotateUserAccountKeysData model) + { + user.Email = model.MasterPasswordUnlockData.Email; + sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash) + .Returns(false); + + var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model); + + Assert.NotEqual(IdentityResult.Success, result); + } + [Theory, BitAutoData] + public async Task ThrowsWhenUserIsNull(SutProvider<RotateUserAccountKeysCommand> sutProvider, + RotateUserAccountKeysData model) + { + await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(null, model)); + } + [Theory, BitAutoData] + public async Task RejectsEmailChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, + RotateUserAccountKeysData model) + { + user.Kdf = Enums.KdfType.Argon2id; + user.KdfIterations = 3; + user.KdfMemory = 64; + user.KdfParallelism = 4; + + model.MasterPasswordUnlockData.Email = user.Email + ".different-domain"; + model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id; + model.MasterPasswordUnlockData.KdfIterations = 3; + model.MasterPasswordUnlockData.KdfMemory = 64; + model.MasterPasswordUnlockData.KdfParallelism = 4; + sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash) + .Returns(true); + await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model)); + } + + [Theory, BitAutoData] + public async Task RejectsKdfChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, + RotateUserAccountKeysData model) + { + user.Kdf = Enums.KdfType.Argon2id; + user.KdfIterations = 3; + user.KdfMemory = 64; + user.KdfParallelism = 4; + + model.MasterPasswordUnlockData.Email = user.Email; + model.MasterPasswordUnlockData.KdfType = Enums.KdfType.PBKDF2_SHA256; + model.MasterPasswordUnlockData.KdfIterations = 600000; + model.MasterPasswordUnlockData.KdfMemory = null; + model.MasterPasswordUnlockData.KdfParallelism = null; + sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash) + .Returns(true); + await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model)); + } + + + [Theory, BitAutoData] + public async Task RejectsPublicKeyChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, + RotateUserAccountKeysData model) + { + user.PublicKey = "old-public"; + user.Kdf = Enums.KdfType.Argon2id; + user.KdfIterations = 3; + user.KdfMemory = 64; + user.KdfParallelism = 4; + + model.AccountPublicKey = "new-public"; + model.MasterPasswordUnlockData.Email = user.Email; + model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id; + model.MasterPasswordUnlockData.KdfIterations = 3; + model.MasterPasswordUnlockData.KdfMemory = 64; + model.MasterPasswordUnlockData.KdfParallelism = 4; + + sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash) + .Returns(true); + + await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model)); + } + + [Theory, BitAutoData] + public async Task RotatesCorrectly(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, + RotateUserAccountKeysData model) + { + user.Kdf = Enums.KdfType.Argon2id; + user.KdfIterations = 3; + user.KdfMemory = 64; + user.KdfParallelism = 4; + + model.MasterPasswordUnlockData.Email = user.Email; + model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id; + model.MasterPasswordUnlockData.KdfIterations = 3; + model.MasterPasswordUnlockData.KdfMemory = 64; + model.MasterPasswordUnlockData.KdfParallelism = 4; + + model.AccountPublicKey = user.PublicKey; + + sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash) + .Returns(true); + + var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model); + + Assert.Equal(IdentityResult.Success, result); + } +} diff --git a/test/Core.Test/KeyManagement/UserKey/RotateUserKeyCommandTests.cs b/test/Core.Test/KeyManagement/UserKey/RotateUserKeyCommandTests.cs index 53263d8805..000fa7e90c 100644 --- a/test/Core.Test/KeyManagement/UserKey/RotateUserKeyCommandTests.cs +++ b/test/Core.Test/KeyManagement/UserKey/RotateUserKeyCommandTests.cs @@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Identity; using NSubstitute; using Xunit; -namespace Bit.Core.Test.KeyManagement.UserFeatures.UserKey; +namespace Bit.Core.Test.KeyManagement.UserKey; [SutProviderCustomize] public class RotateUserKeyCommandTests diff --git a/test/Infrastructure.EFIntegration.Test/Repositories/UserRepositoryTests.cs b/test/Infrastructure.EFIntegration.Test/Repositories/UserRepositoryTests.cs index 066a550fa8..151bd47c44 100644 --- a/test/Infrastructure.EFIntegration.Test/Repositories/UserRepositoryTests.cs +++ b/test/Infrastructure.EFIntegration.Test/Repositories/UserRepositoryTests.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Test.AutoFixture.Attributes; using Bit.Infrastructure.EFIntegration.Test.AutoFixture; @@ -289,4 +290,27 @@ public class UserRepositoryTests var distinctItems = returnedList.Distinct(equalityComparer); Assert.True(!distinctItems.Skip(1).Any()); } + + [CiSkippedTheory, EfUserAutoData] + public async Task UpdateUserKeyAndEncryptedDataAsync_Works_DataMatches(User user, SqlRepo.UserRepository sqlUserRepo) + { + var sqlUser = await sqlUserRepo.CreateAsync(user); + sqlUser.Kdf = KdfType.PBKDF2_SHA256; + sqlUser.KdfIterations = 6_000_000; + sqlUser.KdfMemory = 7_000_000; + sqlUser.KdfParallelism = 8_000_000; + sqlUser.MasterPassword = "masterPasswordHash"; + sqlUser.MasterPasswordHint = "masterPasswordHint"; + sqlUser.Email = "example@example.com"; + + await sqlUserRepo.UpdateUserKeyAndEncryptedDataV2Async(sqlUser, []); + var updatedUser = await sqlUserRepo.GetByIdAsync(sqlUser.Id); + Assert.Equal(sqlUser.Kdf, updatedUser.Kdf); + Assert.Equal(sqlUser.KdfIterations, updatedUser.KdfIterations); + Assert.Equal(sqlUser.KdfMemory, updatedUser.KdfMemory); + Assert.Equal(sqlUser.KdfParallelism, updatedUser.KdfParallelism); + Assert.Equal(sqlUser.MasterPassword, updatedUser.MasterPassword); + Assert.Equal(sqlUser.MasterPasswordHint, updatedUser.MasterPasswordHint); + Assert.Equal(sqlUser.Email, updatedUser.Email); + } } From d563f3f78ab14a04aded397245d0c02a24531da6 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 25 Mar 2025 10:16:06 -0500 Subject: [PATCH 15/19] Fix logic in text handlebars template (#5542) --- .../SecurityTasksNotification.text.hbs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs b/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs index f5493e4503..f6c0921165 100644 --- a/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs +++ b/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs @@ -6,12 +6,14 @@ Launch the Bitwarden extension to review your at-risk passwords. Review at-risk passwords ({{{ReviewPasswordsUrl}}}) -{{#if (eq (length AdminOwnerEmails) 1)}} -This request was initiated by {{AdminOwnerEmails.[0]}}. -{{else}} -This request was initiated by -{{#each AdminOwnerEmails}} - {{#if @last}}and {{/if}}{{this}}{{#unless @last}}, {{/unless}} -{{/each}}. +{{#if AdminOwnerEmails.[0]}} + {{#if AdminOwnerEmails.[1]}} + This request was initiated by + {{#each AdminOwnerEmails}} + {{#if @last}}and {{/if}}{{this}}{{#unless @last}}, {{/unless}} + {{/each}}. + {{else}} + This request was initiated by {{AdminOwnerEmails.[0]}}. + {{/if}} {{/if}} {{/SecurityTasksHtmlLayout}} From f04a3d638b05958957db4e5823d7fb8a511992fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 26 Mar 2025 09:40:13 +0000 Subject: [PATCH 16/19] [PM-18235] Add PersonalOwnershipPolicyRequirement (#5439) * Add PersonalOwnershipPolicyRequirement for managing personal ownership policy * Add tests for PersonalOwnershipPolicyRequirement * Register PersonalOwnershipPolicyRequirement in policy requirement factory * Update ImportCiphersCommand to check PersonalOwnershipPolicyRequirement if the PolicyRequirements flag is enabled Update unit tests * Update CipherService to support PersonalOwnershipPolicyRequirement with feature flag - Add support for checking personal ownership policy using PolicyRequirementQuery when feature flag is enabled - Update CipherService constructor to inject new dependencies - Add tests for personal vault restrictions with and without feature flag * Clean up redundant "Arrange", "Act", and "Assert" comments in test methods * Refactor PersonalOwnershipPolicyRequirementTests method names for clarity - Improve test method names to better describe their purpose and behavior - Rename methods to follow a more descriptive naming convention - No functional changes to the test logic * Remove commented code explaining policy check * Refactor PersonalOwnership Policy Requirement implementation - Add PersonalOwnershipPolicyRequirementFactory to replace static Create method - Simplify policy requirement creation logic - Update PolicyServiceCollectionExtensions to register new factory - Update ImportCiphersCommand to use correct user ID parameter - Remove redundant PersonalOwnershipPolicyRequirementTests * Remove redundant PersonalOwnershipPolicyRequirementTests * Remove unnecessary tests from PersonalOwnershipPolicyRequirementFactoryTests --- .../PersonalOwnershipPolicyRequirement.cs | 26 +++++ .../PolicyServiceCollectionExtensions.cs | 1 + .../ImportFeatures/ImportCiphersCommand.cs | 20 +++- .../Services/Implementations/CipherService.cs | 18 +++- ...lOwnershipPolicyRequirementFactoryTests.cs | 31 ++++++ .../ImportCiphersAsyncCommandTests.cs | 58 ++++++++++- .../Vault/Services/CipherServiceTests.cs | 96 +++++++++++++++++++ 7 files changed, 240 insertions(+), 10 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirementFactoryTests.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs new file mode 100644 index 0000000000..6f3f017bb9 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs @@ -0,0 +1,26 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +/// <summary> +/// Policy requirements for the Disable Personal Ownership policy. +/// </summary> +public class PersonalOwnershipPolicyRequirement : IPolicyRequirement +{ + /// <summary> + /// Indicates whether Personal Ownership is disabled for the user. If true, members are required to save items to an organization. + /// </summary> + public bool DisablePersonalOwnership { get; init; } +} + +public class PersonalOwnershipPolicyRequirementFactory : BasePolicyRequirementFactory<PersonalOwnershipPolicyRequirement> +{ + public override PolicyType PolicyType => PolicyType.PersonalOwnership; + + public override PersonalOwnershipPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails) + { + var result = new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = policyDetails.Any() }; + return result; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index d386006ad2..d330c57291 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -34,5 +34,6 @@ public static class PolicyServiceCollectionExtensions services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, DisableSendPolicyRequirementFactory>(); services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SendOptionsPolicyRequirementFactory>(); services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, ResetPasswordPolicyRequirementFactory>(); + services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, PersonalOwnershipPolicyRequirementFactory>(); } } diff --git a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs index 59d3e5be34..3c58dca183 100644 --- a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs +++ b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs @@ -1,10 +1,13 @@ using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Platform.Push; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Core.Tools.Enums; using Bit.Core.Tools.ImportFeatures.Interfaces; using Bit.Core.Tools.Models.Business; @@ -26,7 +29,8 @@ public class ImportCiphersCommand : IImportCiphersCommand private readonly ICollectionRepository _collectionRepository; private readonly IReferenceEventService _referenceEventService; private readonly ICurrentContext _currentContext; - + private readonly IPolicyRequirementQuery _policyRequirementQuery; + private readonly IFeatureService _featureService; public ImportCiphersCommand( ICipherRepository cipherRepository, @@ -37,7 +41,9 @@ public class ImportCiphersCommand : IImportCiphersCommand IPushNotificationService pushService, IPolicyService policyService, IReferenceEventService referenceEventService, - ICurrentContext currentContext) + ICurrentContext currentContext, + IPolicyRequirementQuery policyRequirementQuery, + IFeatureService featureService) { _cipherRepository = cipherRepository; _folderRepository = folderRepository; @@ -48,9 +54,10 @@ public class ImportCiphersCommand : IImportCiphersCommand _policyService = policyService; _referenceEventService = referenceEventService; _currentContext = currentContext; + _policyRequirementQuery = policyRequirementQuery; + _featureService = featureService; } - public async Task ImportIntoIndividualVaultAsync( List<Folder> folders, List<CipherDetails> ciphers, @@ -58,8 +65,11 @@ public class ImportCiphersCommand : IImportCiphersCommand Guid importingUserId) { // Make sure the user can save new ciphers to their personal vault - var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.PersonalOwnership); - if (anyPersonalOwnershipPolicies) + var isPersonalVaultRestricted = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements) + ? (await _policyRequirementQuery.GetAsync<PersonalOwnershipPolicyRequirement>(importingUserId)).DisablePersonalOwnership + : await _policyService.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.PersonalOwnership); + + if (isPersonalVaultRestricted) { 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 a315528e59..b9daafe599 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -1,5 +1,7 @@ using System.Text.Json; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Context; using Bit.Core.Enums; @@ -41,6 +43,8 @@ public class CipherService : ICipherService private readonly IReferenceEventService _referenceEventService; private readonly ICurrentContext _currentContext; private readonly IGetCipherPermissionsForUserQuery _getCipherPermissionsForUserQuery; + private readonly IPolicyRequirementQuery _policyRequirementQuery; + private readonly IFeatureService _featureService; public CipherService( ICipherRepository cipherRepository, @@ -58,7 +62,9 @@ public class CipherService : ICipherService GlobalSettings globalSettings, IReferenceEventService referenceEventService, ICurrentContext currentContext, - IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery) + IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery, + IPolicyRequirementQuery policyRequirementQuery, + IFeatureService featureService) { _cipherRepository = cipherRepository; _folderRepository = folderRepository; @@ -76,6 +82,8 @@ public class CipherService : ICipherService _referenceEventService = referenceEventService; _currentContext = currentContext; _getCipherPermissionsForUserQuery = getCipherPermissionsForUserQuery; + _policyRequirementQuery = policyRequirementQuery; + _featureService = featureService; } public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate, @@ -143,9 +151,11 @@ public class CipherService : ICipherService } else { - // Make sure the user can save new ciphers to their personal vault - var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership); - if (anyPersonalOwnershipPolicies) + var isPersonalVaultRestricted = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements) + ? (await _policyRequirementQuery.GetAsync<PersonalOwnershipPolicyRequirement>(savingUserId)).DisablePersonalOwnership + : await _policyService.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership); + + if (isPersonalVaultRestricted) { 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/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirementFactoryTests.cs new file mode 100644 index 0000000000..2ce75ca61e --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirementFactoryTests.cs @@ -0,0 +1,31 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +[SutProviderCustomize] +public class PersonalOwnershipPolicyRequirementFactoryTests +{ + [Theory, BitAutoData] + public void DisablePersonalOwnership_WithNoPolicies_ReturnsFalse(SutProvider<PersonalOwnershipPolicyRequirementFactory> sutProvider) + { + var actual = sutProvider.Sut.Create([]); + + Assert.False(actual.DisablePersonalOwnership); + } + + [Theory, BitAutoData] + public void DisablePersonalOwnership_WithPersonalOwnershipPolicies_ReturnsTrue( + [PolicyDetails(PolicyType.PersonalOwnership)] PolicyDetails[] policies, + SutProvider<PersonalOwnershipPolicyRequirementFactory> sutProvider) + { + var actual = sutProvider.Sut.Create(policies); + + Assert.True(actual.DisablePersonalOwnership); + } +} diff --git a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs index 5e7a30d814..89e6d152cc 100644 --- a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs +++ b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs @@ -1,10 +1,13 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Platform.Push; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Core.Test.AutoFixture.CipherFixtures; using Bit.Core.Tools.Enums; using Bit.Core.Tools.ImportFeatures; @@ -18,7 +21,6 @@ using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; - namespace Bit.Core.Test.Tools.ImportFeatures; [UserCipherCustomize] @@ -51,6 +53,34 @@ public class ImportCiphersAsyncCommandTests await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId); } + [Theory, BitAutoData] + public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyDisabled_Success( + Guid importingUserId, + List<CipherDetails> ciphers, + SutProvider<ImportCiphersCommand> sutProvider) + { + sutProvider.GetDependency<IFeatureService>() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + sutProvider.GetDependency<IPolicyRequirementQuery>() + .GetAsync<PersonalOwnershipPolicyRequirement>(importingUserId) + .Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = false }); + + sutProvider.GetDependency<IFolderRepository>() + .GetManyByUserIdAsync(importingUserId) + .Returns(new List<Folder>()); + + var folders = new List<Folder> { new Folder { UserId = importingUserId } }; + + var folderRelationships = new List<KeyValuePair<int, int>>(); + + await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId); + + await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(ciphers, Arg.Any<List<Folder>>()); + await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId); + } + [Theory, BitAutoData] public async Task ImportIntoIndividualVaultAsync_ThrowsBadRequestException( List<Folder> folders, @@ -73,6 +103,32 @@ public class ImportCiphersAsyncCommandTests Assert.Equal("You cannot import items into your personal vault because you are a member of an organization which forbids it.", exception.Message); } + [Theory, BitAutoData] + public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyEnabled_ThrowsBadRequestException( + List<Folder> folders, + List<CipherDetails> ciphers, + SutProvider<ImportCiphersCommand> sutProvider) + { + var userId = Guid.NewGuid(); + folders.ForEach(f => f.UserId = userId); + ciphers.ForEach(c => c.UserId = userId); + + sutProvider.GetDependency<IFeatureService>() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + sutProvider.GetDependency<IPolicyRequirementQuery>() + .GetAsync<PersonalOwnershipPolicyRequirement>(userId) + .Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = true }); + + var folderRelationships = new List<KeyValuePair<int, int>>(); + + var exception = await Assert.ThrowsAsync<BadRequestException>(() => + sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, userId)); + + Assert.Equal("You cannot import items into your personal vault because you are a member of an organization which forbids it.", exception.Message); + } + [Theory, BitAutoData] public async Task ImportIntoOrganizationalVaultAsync_Success( Organization organization, diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs index 3ef29146c2..a7dcbddcea 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -1,5 +1,9 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; @@ -107,6 +111,98 @@ public class CipherServiceTests await sutProvider.GetDependency<ICipherRepository>().Received(1).ReplaceAsync(cipherDetails); } + [Theory] + [BitAutoData] + public async Task SaveDetailsAsync_PersonalVault_WithDisablePersonalOwnershipPolicyEnabled_Throws( + SutProvider<CipherService> sutProvider, + CipherDetails cipher, + Guid savingUserId) + { + cipher.Id = default; + cipher.UserId = savingUserId; + cipher.OrganizationId = null; + + sutProvider.GetDependency<IPolicyService>() + .AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership) + .Returns(true); + + var exception = await Assert.ThrowsAsync<BadRequestException>( + () => sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null)); + Assert.Contains("restricted from saving items to your personal vault", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task SaveDetailsAsync_PersonalVault_WithDisablePersonalOwnershipPolicyDisabled_Succeeds( + SutProvider<CipherService> sutProvider, + CipherDetails cipher, + Guid savingUserId) + { + cipher.Id = default; + cipher.UserId = savingUserId; + cipher.OrganizationId = null; + + sutProvider.GetDependency<IPolicyService>() + .AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership) + .Returns(false); + + await sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null); + + await sutProvider.GetDependency<ICipherRepository>() + .Received(1) + .CreateAsync(cipher); + } + + [Theory] + [BitAutoData] + public async Task SaveDetailsAsync_PersonalVault_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyEnabled_Throws( + SutProvider<CipherService> sutProvider, + CipherDetails cipher, + Guid savingUserId) + { + cipher.Id = default; + cipher.UserId = savingUserId; + cipher.OrganizationId = null; + + sutProvider.GetDependency<IFeatureService>() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + sutProvider.GetDependency<IPolicyRequirementQuery>() + .GetAsync<PersonalOwnershipPolicyRequirement>(savingUserId) + .Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = true }); + + var exception = await Assert.ThrowsAsync<BadRequestException>( + () => sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null)); + Assert.Contains("restricted from saving items to your personal vault", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task SaveDetailsAsync_PersonalVault_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyDisabled_Succeeds( + SutProvider<CipherService> sutProvider, + CipherDetails cipher, + Guid savingUserId) + { + cipher.Id = default; + cipher.UserId = savingUserId; + cipher.OrganizationId = null; + + sutProvider.GetDependency<IFeatureService>() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + sutProvider.GetDependency<IPolicyRequirementQuery>() + .GetAsync<PersonalOwnershipPolicyRequirement>(savingUserId) + .Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = false }); + + await sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null); + + await sutProvider.GetDependency<ICipherRepository>() + .Received(1) + .CreateAsync(cipher); + } + [Theory] [BitAutoData("")] [BitAutoData("Correct Time")] From 6f227c31e2be364d0c147b454dd87c21e4179030 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann <mail@quexten.com> Date: Wed, 26 Mar 2025 15:10:35 +0100 Subject: [PATCH 17/19] Sort km feature flags (#5557) --- src/Core/Constants.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index e41391b173..c629881458 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -129,19 +129,25 @@ public static class FeatureFlagKeys /* Auth Team */ public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence"; + /* Key Management Team */ + public const string SSHKeyItemVaultItem = "ssh-key-vault-item"; + public const string SSHAgent = "ssh-agent"; + public const string SSHVersionCheckQAOverride = "ssh-version-check-qa-override"; + public const string Argon2Default = "argon2-default"; + public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service"; + public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration"; + public const string UserkeyRotationV2 = "userkey-rotation-v2"; + + /* Unsorted */ public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection"; public const string DuoRedirect = "duo-redirect"; public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email"; public const string EmailVerification = "email-verification"; public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays"; - public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service"; public const string InlineMenuFieldQualification = "inline-menu-field-qualification"; public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements"; public const string DeviceTrustLogging = "pm-8285-device-trust-logging"; - public const string SSHKeyItemVaultItem = "ssh-key-vault-item"; - public const string SSHAgent = "ssh-agent"; - public const string SSHVersionCheckQAOverride = "ssh-version-check-qa-override"; public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token"; public const string IdpAutoSubmitLogin = "idp-auto-submit-login"; public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; @@ -162,13 +168,10 @@ public static class FeatureFlagKeys public const string NewDeviceVerification = "new-device-verification"; public const string MacOsNativeCredentialSync = "macos-native-credential-sync"; public const string InlineMenuTotp = "inline-menu-totp"; - public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration"; public const string AppReviewPrompt = "app-review-prompt"; public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs"; - public const string Argon2Default = "argon2-default"; public const string UsePricingService = "use-pricing-service"; public const string RecordInstallationLastActivityDate = "installation-last-activity-date"; - public const string UserkeyRotationV2 = "userkey-rotation-v2"; public const string EnablePasswordManagerSyncAndroid = "enable-password-manager-sync-android"; public const string EnablePasswordManagerSynciOS = "enable-password-manager-sync-ios"; public const string AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner"; From d4b00583728f1e85de3ba8e0b62ee16767842855 Mon Sep 17 00:00:00 2001 From: Matt Bishop <mbishop@bitwarden.com> Date: Wed, 26 Mar 2025 11:44:05 -0400 Subject: [PATCH 18/19] Organization integrations and configuration database schemas (#5553) * Organization integrations and configuration database schemas * Format EF files --- .../Entities/OrganizationIntegration.cs | 18 + .../OrganizationIntegrationConfiguration.cs | 19 + .../AdminConsole/Enums/IntegrationType.cs | 7 + ...ionConfigurationEntityTypeConfiguration.cs | 17 + ...ationIntegrationEntityTypeConfiguration.cs | 26 + .../Models/OrganizationIntegration.cs | 16 + .../OrganizationIntegrationConfiguration.cs | 16 + ...EventTypeOrganizationIdIntegrationType.sql | 22 + .../dbo/Tables/OrganizationIntegration.sql | 20 + .../OrganizationIntegrationConfiguration.sql | 13 + ...ganizationIntegrationConfigurationView.sql | 6 + .../dbo/Views/OrganizationIntegrationView.sql | 6 + ...2025-03-24_00_OrganizationIntegrations.sql | 101 + ...31708_OrganizationIntegrations.Designer.cs | 3101 ++++++++++++++++ ...20250325231708_OrganizationIntegrations.cs | 89 + .../DatabaseContextModelSnapshot.cs | 92 +- ...31701_OrganizationIntegrations.Designer.cs | 3107 +++++++++++++++++ ...20250325231701_OrganizationIntegrations.cs | 84 + .../DatabaseContextModelSnapshot.cs | 92 +- ...31714_OrganizationIntegrations.Designer.cs | 3090 ++++++++++++++++ ...20250325231714_OrganizationIntegrations.cs | 84 + .../DatabaseContextModelSnapshot.cs | 92 +- 22 files changed, 10106 insertions(+), 12 deletions(-) create mode 100644 src/Core/AdminConsole/Entities/OrganizationIntegration.cs create mode 100644 src/Core/AdminConsole/Entities/OrganizationIntegrationConfiguration.cs create mode 100644 src/Core/AdminConsole/Enums/IntegrationType.cs create mode 100644 src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationConfigurationEntityTypeConfiguration.cs create mode 100644 src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationEntityTypeConfiguration.cs create mode 100644 src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegration.cs create mode 100644 src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegrationConfiguration.cs create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_ReadManyByEventTypeOrganizationIdIntegrationType.sql create mode 100644 src/Sql/dbo/Tables/OrganizationIntegration.sql create mode 100644 src/Sql/dbo/Tables/OrganizationIntegrationConfiguration.sql create mode 100644 src/Sql/dbo/Views/OrganizationIntegrationConfigurationView.sql create mode 100644 src/Sql/dbo/Views/OrganizationIntegrationView.sql create mode 100644 util/Migrator/DbScripts/2025-03-24_00_OrganizationIntegrations.sql create mode 100644 util/MySqlMigrations/Migrations/20250325231708_OrganizationIntegrations.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250325231708_OrganizationIntegrations.cs create mode 100644 util/PostgresMigrations/Migrations/20250325231701_OrganizationIntegrations.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250325231701_OrganizationIntegrations.cs create mode 100644 util/SqliteMigrations/Migrations/20250325231714_OrganizationIntegrations.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250325231714_OrganizationIntegrations.cs diff --git a/src/Core/AdminConsole/Entities/OrganizationIntegration.cs b/src/Core/AdminConsole/Entities/OrganizationIntegration.cs new file mode 100644 index 0000000000..18f8be8667 --- /dev/null +++ b/src/Core/AdminConsole/Entities/OrganizationIntegration.cs @@ -0,0 +1,18 @@ +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Utilities; + +#nullable enable + +namespace Bit.Core.AdminConsole.Entities; + +public class OrganizationIntegration : ITableObject<Guid> +{ + public Guid Id { get; set; } + public Guid OrganizationId { get; set; } + public IntegrationType Type { get; set; } + public string? Configuration { get; set; } + 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/AdminConsole/Entities/OrganizationIntegrationConfiguration.cs b/src/Core/AdminConsole/Entities/OrganizationIntegrationConfiguration.cs new file mode 100644 index 0000000000..7592d0c763 --- /dev/null +++ b/src/Core/AdminConsole/Entities/OrganizationIntegrationConfiguration.cs @@ -0,0 +1,19 @@ +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Utilities; + +#nullable enable + +namespace Bit.Core.AdminConsole.Entities; + +public class OrganizationIntegrationConfiguration : ITableObject<Guid> +{ + public Guid Id { get; set; } + public Guid OrganizationIntegrationId { get; set; } + public EventType EventType { get; set; } + public string? Configuration { get; set; } + public string? Template { get; set; } + 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/AdminConsole/Enums/IntegrationType.cs b/src/Core/AdminConsole/Enums/IntegrationType.cs new file mode 100644 index 0000000000..16c7818dee --- /dev/null +++ b/src/Core/AdminConsole/Enums/IntegrationType.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Enums; + +public enum IntegrationType : int +{ + Slack = 1, + Webhook = 2, +} diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationConfigurationEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationConfigurationEntityTypeConfiguration.cs new file mode 100644 index 0000000000..29712f5e38 --- /dev/null +++ b/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationConfigurationEntityTypeConfiguration.cs @@ -0,0 +1,17 @@ +using Bit.Infrastructure.EntityFramework.AdminConsole.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Bit.Infrastructure.EntityFramework.AdminConsole.Configurations; + +public class OrganizationIntegrationConfigurationEntityTypeConfiguration : IEntityTypeConfiguration<OrganizationIntegrationConfiguration> +{ + public void Configure(EntityTypeBuilder<OrganizationIntegrationConfiguration> builder) + { + builder + .Property(p => p.Id) + .ValueGeneratedNever(); + + builder.ToTable(nameof(OrganizationIntegrationConfiguration)); + } +} diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationEntityTypeConfiguration.cs new file mode 100644 index 0000000000..c2134c1b7d --- /dev/null +++ b/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationEntityTypeConfiguration.cs @@ -0,0 +1,26 @@ +using Bit.Infrastructure.EntityFramework.AdminConsole.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Bit.Infrastructure.EntityFramework.AdminConsole.Configurations; + +public class OrganizationIntegrationEntityTypeConfiguration : IEntityTypeConfiguration<OrganizationIntegration> +{ + public void Configure(EntityTypeBuilder<OrganizationIntegration> builder) + { + builder + .Property(p => p.Id) + .ValueGeneratedNever(); + + builder + .HasIndex(p => p.OrganizationId) + .IsClustered(false); + + builder + .HasIndex(p => new { p.OrganizationId, p.Type }) + .IsUnique() + .IsClustered(false); + + builder.ToTable(nameof(OrganizationIntegration)); + } +} diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegration.cs b/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegration.cs new file mode 100644 index 0000000000..db81b81166 --- /dev/null +++ b/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegration.cs @@ -0,0 +1,16 @@ +using AutoMapper; + +namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models; + +public class OrganizationIntegration : Core.AdminConsole.Entities.OrganizationIntegration +{ + public virtual Organization Organization { get; set; } +} + +public class OrganizationIntegrationMapperProfile : Profile +{ + public OrganizationIntegrationMapperProfile() + { + CreateMap<Core.AdminConsole.Entities.OrganizationIntegration, OrganizationIntegration>().ReverseMap(); + } +} diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegrationConfiguration.cs b/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegrationConfiguration.cs new file mode 100644 index 0000000000..465a49dc02 --- /dev/null +++ b/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegrationConfiguration.cs @@ -0,0 +1,16 @@ +using AutoMapper; + +namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models; + +public class OrganizationIntegrationConfiguration : Core.AdminConsole.Entities.OrganizationIntegrationConfiguration +{ + public virtual OrganizationIntegration OrganizationIntegration { get; set; } +} + +public class OrganizationIntegrationConfigurationMapperProfile : Profile +{ + public OrganizationIntegrationConfigurationMapperProfile() + { + CreateMap<Core.AdminConsole.Entities.OrganizationIntegrationConfiguration, OrganizationIntegrationConfiguration>().ReverseMap(); + } +} diff --git a/src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_ReadManyByEventTypeOrganizationIdIntegrationType.sql b/src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_ReadManyByEventTypeOrganizationIdIntegrationType.sql new file mode 100644 index 0000000000..113aa2e529 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_ReadManyByEventTypeOrganizationIdIntegrationType.sql @@ -0,0 +1,22 @@ +CREATE PROCEDURE [dbo].[OrganizationIntegrationConfiguration_ReadManyByEventTypeOrganizationIdIntegrationType] + @EventType SMALLINT, + @OrganizationId UNIQUEIDENTIFIER, + @IntegrationType SMALLINT +AS +BEGIN + SET NOCOUNT ON + + SELECT + oic.* + FROM + [dbo].[OrganizationIntegrationConfigurationView] oic + INNER JOIN + [dbo].[OrganizationIntegration] oi ON oi.[Id] = oic.[OrganizationIntegrationId] + WHERE + oic.[EventType] = @EventType + AND + oi.[OrganizationId] = @OrganizationId + AND + oi.[Type] = @IntegrationType +END +GO diff --git a/src/Sql/dbo/Tables/OrganizationIntegration.sql b/src/Sql/dbo/Tables/OrganizationIntegration.sql new file mode 100644 index 0000000000..8ac289c303 --- /dev/null +++ b/src/Sql/dbo/Tables/OrganizationIntegration.sql @@ -0,0 +1,20 @@ +CREATE TABLE [dbo].[OrganizationIntegration] +( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [Type] SMALLINT NOT NULL, + [Configuration] VARCHAR (MAX) NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [RevisionDate] DATETIME2 (7) NOT NULL, + CONSTRAINT [PK_OrganizationIntegration] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_OrganizationIntegration_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) +); +GO + +CREATE NONCLUSTERED INDEX [IX_OrganizationIntegration_OrganizationId] + ON [dbo].[OrganizationIntegration]([OrganizationId] ASC); +GO + +CREATE UNIQUE INDEX [IX_OrganizationIntegration_Organization_Type] + ON [dbo].[OrganizationIntegration]([OrganizationId], [Type]); +GO diff --git a/src/Sql/dbo/Tables/OrganizationIntegrationConfiguration.sql b/src/Sql/dbo/Tables/OrganizationIntegrationConfiguration.sql new file mode 100644 index 0000000000..9dbb2341a7 --- /dev/null +++ b/src/Sql/dbo/Tables/OrganizationIntegrationConfiguration.sql @@ -0,0 +1,13 @@ +CREATE TABLE [dbo].[OrganizationIntegrationConfiguration] +( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationIntegrationId] UNIQUEIDENTIFIER NOT NULL, + [EventType] SMALLINT NOT NULL, + [Configuration] VARCHAR (MAX) NULL, + [Template] VARCHAR (MAX) NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [RevisionDate] DATETIME2 (7) NOT NULL, + CONSTRAINT [PK_OrganizationIntegrationConfiguration] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_OrganizationIntegrationConfiguration_OrganizationIntegration] FOREIGN KEY ([OrganizationIntegrationId]) REFERENCES [dbo].[OrganizationIntegration] ([Id]) +); +GO diff --git a/src/Sql/dbo/Views/OrganizationIntegrationConfigurationView.sql b/src/Sql/dbo/Views/OrganizationIntegrationConfigurationView.sql new file mode 100644 index 0000000000..4f39fbc8f3 --- /dev/null +++ b/src/Sql/dbo/Views/OrganizationIntegrationConfigurationView.sql @@ -0,0 +1,6 @@ +CREATE VIEW [dbo].[OrganizationIntegrationConfigurationView] +AS + SELECT + * + FROM + [dbo].[OrganizationIntegrationConfiguration] diff --git a/src/Sql/dbo/Views/OrganizationIntegrationView.sql b/src/Sql/dbo/Views/OrganizationIntegrationView.sql new file mode 100644 index 0000000000..31e005d5d2 --- /dev/null +++ b/src/Sql/dbo/Views/OrganizationIntegrationView.sql @@ -0,0 +1,6 @@ +CREATE VIEW [dbo].[OrganizationIntegrationView] +AS + SELECT + * + FROM + [dbo].[OrganizationIntegration] diff --git a/util/Migrator/DbScripts/2025-03-24_00_OrganizationIntegrations.sql b/util/Migrator/DbScripts/2025-03-24_00_OrganizationIntegrations.sql new file mode 100644 index 0000000000..56d4d465d2 --- /dev/null +++ b/util/Migrator/DbScripts/2025-03-24_00_OrganizationIntegrations.sql @@ -0,0 +1,101 @@ +-- OrganizationIntegration + +-- Table +IF OBJECT_ID('[dbo].[OrganizationIntegration]') IS NULL +BEGIN + CREATE TABLE [dbo].[OrganizationIntegration] + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [Type] SMALLINT NOT NULL, + [Configuration] VARCHAR (MAX) NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [RevisionDate] DATETIME2 (7) NOT NULL, + CONSTRAINT [PK_OrganizationIntegration] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_OrganizationIntegration_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) + ); + + CREATE NONCLUSTERED INDEX [IX_OrganizationIntegration_OrganizationId] + ON [dbo].[OrganizationIntegration]([OrganizationId] ASC); + + CREATE UNIQUE INDEX [IX_OrganizationIntegration_Organization_Type] + ON [dbo].[OrganizationIntegration]([OrganizationId], [Type]); +END +GO + +-- View +IF EXISTS(SELECT * +FROM sys.views +WHERE [Name] = 'OrganizationIntegrationView') +BEGIN + DROP VIEW [dbo].[OrganizationIntegrationView]; +END +GO + +CREATE VIEW [dbo].[OrganizationIntegrationView] +AS + SELECT + * + FROM + [dbo].[OrganizationIntegration] +GO + +-- OrganizationIntegrationConfiguration + +-- Table +IF OBJECT_ID('[dbo].[OrganizationIntegrationConfiguration]') IS NULL +BEGIN + CREATE TABLE [dbo].[OrganizationIntegrationConfiguration] + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationIntegrationId] UNIQUEIDENTIFIER NOT NULL, + [EventType] SMALLINT NOT NULL, + [Configuration] VARCHAR (MAX) NULL, + [Template] VARCHAR (MAX) NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [RevisionDate] DATETIME2 (7) NOT NULL, + CONSTRAINT [PK_OrganizationIntegrationConfiguration] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_OrganizationIntegrationConfiguration_OrganizationIntegration] FOREIGN KEY ([OrganizationIntegrationId]) REFERENCES [dbo].[OrganizationIntegration] ([Id]) + ); +END +GO + +-- View +IF EXISTS(SELECT * +FROM sys.views +WHERE [Name] = 'OrganizationIntegrationConfigurationView') +BEGIN + DROP VIEW [dbo].[OrganizationIntegrationConfigurationView]; +END +GO + +CREATE VIEW [dbo].[OrganizationIntegrationConfigurationView] +AS + SELECT + * + FROM + [dbo].[OrganizationIntegrationConfiguration] +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationIntegrationConfiguration_ReadManyByEventTypeOrganizationIdIntegrationType] + @EventType SMALLINT, + @OrganizationId UNIQUEIDENTIFIER, + @IntegrationType SMALLINT +AS +BEGIN + SET NOCOUNT ON + + SELECT + oic.* + FROM + [dbo].[OrganizationIntegrationConfigurationView] oic + INNER JOIN + [dbo].[OrganizationIntegration] oi ON oi.[Id] = oic.[OrganizationIntegrationId] + WHERE + oic.[EventType] = @EventType + AND + oi.[OrganizationId] = @OrganizationId + AND + oi.[Type] = @IntegrationType +END +GO diff --git a/util/MySqlMigrations/Migrations/20250325231708_OrganizationIntegrations.Designer.cs b/util/MySqlMigrations/Migrations/20250325231708_OrganizationIntegrations.Designer.cs new file mode 100644 index 0000000000..0968a87104 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250325231708_OrganizationIntegrations.Designer.cs @@ -0,0 +1,3101 @@ +// <auto-generated /> +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("20250325231708_OrganizationIntegrations")] + partial class OrganizationIntegrations + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<bool>("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property<string>("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property<string>("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<string>("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<string>("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<string>("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property<string>("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<string>("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<bool>("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property<DateTime?>("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property<byte?>("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property<string>("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<string>("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<string>("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<string>("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property<bool>("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("LimitItemDeletion") + .HasColumnType("tinyint(1)"); + + b.Property<int?>("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property<int?>("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property<int?>("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property<short?>("MaxCollections") + .HasColumnType("smallint"); + + b.Property<short?>("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<DateTime?>("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property<string>("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<byte>("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property<string>("PrivateKey") + .HasColumnType("longtext"); + + b.Property<string>("PublicKey") + .HasColumnType("longtext"); + + b.Property<string>("ReferenceData") + .HasColumnType("longtext"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<int?>("Seats") + .HasColumnType("int"); + + b.Property<bool>("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property<int?>("SmSeats") + .HasColumnType("int"); + + b.Property<int?>("SmServiceAccounts") + .HasColumnType("int"); + + b.Property<byte>("Status") + .HasColumnType("tinyint unsigned"); + + b.Property<long?>("Storage") + .HasColumnType("bigint"); + + b.Property<string>("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property<bool>("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("UseRiskInsights") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("UseSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<string>("Configuration") + .HasColumnType("longtext"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<int>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<string>("Configuration") + .HasColumnType("longtext"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<int>("EventType") + .HasColumnType("int"); + + b.Property<Guid>("OrganizationIntegrationId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Data") + .HasColumnType("longtext"); + + b.Property<bool>("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<byte>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<string>("BillingEmail") + .HasColumnType("longtext"); + + b.Property<string>("BillingPhone") + .HasColumnType("longtext"); + + b.Property<string>("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property<string>("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property<string>("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property<string>("BusinessCountry") + .HasColumnType("longtext"); + + b.Property<string>("BusinessName") + .HasColumnType("longtext"); + + b.Property<string>("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("DiscountId") + .HasColumnType("longtext"); + + b.Property<bool>("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property<byte?>("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property<string>("GatewayCustomerId") + .HasColumnType("longtext"); + + b.Property<string>("GatewaySubscriptionId") + .HasColumnType("longtext"); + + b.Property<string>("Name") + .HasColumnType("longtext"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<byte>("Status") + .HasColumnType("tinyint unsigned"); + + b.Property<byte>("Type") + .HasColumnType("tinyint unsigned"); + + b.Property<bool>("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Key") + .HasColumnType("longtext"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<Guid>("ProviderId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Email") + .HasColumnType("longtext"); + + b.Property<string>("Key") + .HasColumnType("longtext"); + + b.Property<string>("Permissions") + .HasColumnType("longtext"); + + b.Property<Guid>("ProviderId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<byte>("Status") + .HasColumnType("tinyint unsigned"); + + b.Property<byte>("Type") + .HasColumnType("tinyint unsigned"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<string>("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property<bool?>("Approved") + .HasColumnType("tinyint(1)"); + + b.Property<DateTime?>("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Key") + .HasColumnType("longtext"); + + b.Property<string>("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<string>("PublicKey") + .HasColumnType("longtext"); + + b.Property<string>("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property<string>("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<byte>("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property<string>("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<DateTime?>("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property<Guid?>("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property<byte>("Type") + .HasColumnType("tinyint unsigned"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property<Guid?>("GranteeId") + .HasColumnType("char(36)"); + + b.Property<Guid>("GrantorId") + .HasColumnType("char(36)"); + + b.Property<string>("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property<DateTime?>("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property<DateTime?>("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<byte>("Status") + .HasColumnType("tinyint unsigned"); + + b.Property<byte>("Type") + .HasColumnType("tinyint unsigned"); + + b.Property<int>("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<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id")); + + b.Property<string>("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property<DateTime?>("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property<string>("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property<DateTime?>("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property<string>("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property<string>("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property<string>("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<long>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id")); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Data") + .HasColumnType("longtext"); + + b.Property<bool>("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("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<long>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id")); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("ExternalId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<Guid>("AaGuid") + .HasColumnType("char(36)"); + + b.Property<int>("Counter") + .HasColumnType("int"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property<string>("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property<string>("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property<string>("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property<string>("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<string>("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<bool>("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property<string>("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<DateTime?>("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<string>("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<int?>("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property<short?>("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<byte>("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property<Guid>("ProviderId") + .HasColumnType("char(36)"); + + b.Property<int>("Seats") + .HasColumnType("int"); + + b.Property<byte>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<Guid>("InstallationId") + .HasColumnType("char(36)"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<DateTime?>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<int>("AssignedSeats") + .HasColumnType("int"); + + b.Property<Guid?>("ClientId") + .HasColumnType("char(36)"); + + b.Property<string>("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<DateTime>("Created") + .HasColumnType("datetime(6)"); + + b.Property<string>("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<string>("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<string>("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<Guid>("ProviderId") + .HasColumnType("char(36)"); + + b.Property<decimal>("Total") + .HasColumnType("decimal(65,30)"); + + b.Property<int>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<int?>("AllocatedSeats") + .HasColumnType("int"); + + b.Property<byte>("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property<Guid>("ProviderId") + .HasColumnType("char(36)"); + + b.Property<int?>("PurchasedSeats") + .HasColumnType("int"); + + b.Property<int?>("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<string>("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property<DateTime?>("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property<DateTime>("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property<long?>("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property<byte[]>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("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<Guid>("CollectionId") + .HasColumnType("char(36)"); + + b.Property<Guid>("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<Guid>("CollectionId") + .HasColumnType("char(36)"); + + b.Property<Guid>("GroupId") + .HasColumnType("char(36)"); + + b.Property<bool>("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("Manage") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property<Guid>("CollectionId") + .HasColumnType("char(36)"); + + b.Property<Guid>("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property<bool>("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("Manage") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property<bool>("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property<string>("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property<string>("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property<string>("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<string>("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<byte>("Type") + .HasColumnType("tinyint unsigned"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<Guid?>("ActingUserId") + .HasColumnType("char(36)"); + + b.Property<Guid?>("CipherId") + .HasColumnType("char(36)"); + + b.Property<Guid?>("CollectionId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("Date") + .HasColumnType("datetime(6)"); + + b.Property<byte?>("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property<string>("DomainName") + .HasColumnType("longtext"); + + b.Property<Guid?>("GroupId") + .HasColumnType("char(36)"); + + b.Property<Guid?>("InstallationId") + .HasColumnType("char(36)"); + + b.Property<string>("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<Guid?>("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property<Guid?>("PolicyId") + .HasColumnType("char(36)"); + + b.Property<Guid?>("ProviderId") + .HasColumnType("char(36)"); + + b.Property<Guid?>("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property<Guid?>("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property<Guid?>("SecretId") + .HasColumnType("char(36)"); + + b.Property<Guid?>("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property<byte?>("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property<int>("Type") + .HasColumnType("int"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("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<Guid>("GroupId") + .HasColumnType("char(36)"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<string>("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<byte>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<string>("Config") + .HasColumnType("longtext"); + + b.Property<bool>("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<byte>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property<int>("JobRunCount") + .HasColumnType("int"); + + b.Property<DateTime?>("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property<DateTime>("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<string>("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property<DateTime?>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<string>("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property<DateTime?>("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property<byte?>("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property<Guid?>("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property<Guid?>("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property<Guid>("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property<bool>("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property<DateTime?>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<bool>("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property<string>("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property<string>("Key") + .HasColumnType("longtext"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<string>("Permissions") + .HasColumnType("longtext"); + + b.Property<string>("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<short>("Status") + .HasColumnType("smallint"); + + b.Property<byte>("Type") + .HasColumnType("tinyint unsigned"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<int>("AccessCount") + .HasColumnType("int"); + + b.Property<Guid?>("CipherId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Data") + .HasColumnType("longtext"); + + b.Property<DateTime>("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property<bool>("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property<DateTime?>("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property<bool?>("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property<string>("Key") + .HasColumnType("longtext"); + + b.Property<int?>("MaxAccessCount") + .HasColumnType("int"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<string>("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<byte>("Type") + .HasColumnType("tinyint unsigned"); + + b.Property<Guid?>("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<string>("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property<bool>("Active") + .HasColumnType("tinyint(1)"); + + b.Property<string>("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<string>("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property<decimal>("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property<string>("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<decimal>("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property<byte?>("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property<string>("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<byte?>("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property<Guid?>("ProviderId") + .HasColumnType("char(36)"); + + b.Property<bool?>("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property<decimal?>("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property<byte>("Type") + .HasColumnType("tinyint unsigned"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<DateTime>("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property<string>("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property<string>("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property<bool>("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property<string>("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property<string>("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property<int>("FailedLoginCount") + .HasColumnType("int"); + + b.Property<bool>("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property<byte?>("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property<string>("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<string>("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<byte>("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property<int>("KdfIterations") + .HasColumnType("int"); + + b.Property<int?>("KdfMemory") + .HasColumnType("int"); + + b.Property<int?>("KdfParallelism") + .HasColumnType("int"); + + b.Property<string>("Key") + .HasColumnType("longtext"); + + b.Property<DateTime?>("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property<DateTime?>("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property<DateTime?>("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property<DateTime?>("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property<DateTime?>("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property<string>("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property<string>("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<short?>("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property<string>("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<bool>("Premium") + .HasColumnType("tinyint(1)"); + + b.Property<DateTime?>("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("PrivateKey") + .HasColumnType("longtext"); + + b.Property<string>("PublicKey") + .HasColumnType("longtext"); + + b.Property<string>("ReferenceData") + .HasColumnType("longtext"); + + b.Property<DateTime?>("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property<long?>("Storage") + .HasColumnType("bigint"); + + b.Property<string>("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property<string>("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property<bool>("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property<bool>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<string>("Body") + .HasMaxLength(3000) + .HasColumnType("varchar(3000)"); + + b.Property<byte>("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<bool>("Global") + .HasColumnType("tinyint(1)"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<byte>("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<Guid?>("TaskId") + .HasColumnType("char(36)"); + + b.Property<string>("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property<Guid?>("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<Guid>("UserId") + .HasColumnType("char(36)"); + + b.Property<Guid>("NotificationId") + .HasColumnType("char(36)"); + + b.Property<DateTime?>("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property<DateTime?>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property<bool>("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property<string>("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property<DateTime?>("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property<bool>("Read") + .HasColumnType("tinyint(1)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<bool>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<string>("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property<DateTime?>("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property<string>("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<DateTime?>("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Name") + .HasColumnType("longtext"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<DateTime?>("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Key") + .HasColumnType("longtext"); + + b.Property<string>("Note") + .HasColumnType("longtext"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Name") + .HasColumnType("longtext"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<string>("Attachments") + .HasColumnType("longtext"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Data") + .HasColumnType("longtext"); + + b.Property<DateTime?>("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Favorites") + .HasColumnType("longtext"); + + b.Property<string>("Folders") + .HasColumnType("longtext"); + + b.Property<string>("Key") + .HasColumnType("longtext"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<byte?>("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<byte>("Type") + .HasColumnType("tinyint unsigned"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("Name") + .HasColumnType("longtext"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<Guid?>("CipherId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<byte>("Status") + .HasColumnType("tinyint unsigned"); + + b.Property<byte>("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<Guid>("ProjectsId") + .HasColumnType("char(36)"); + + b.Property<Guid>("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<Guid?>("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property<Guid?>("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<Guid?>("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property<Guid?>("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<Guid?>("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property<Guid?>("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<Guid?>("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property<Guid?>("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<Guid?>("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property<Guid?>("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<Guid?>("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property<Guid?>("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<Guid?>("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property<Guid?>("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<Guid?>("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property<Guid?>("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"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20250325231708_OrganizationIntegrations.cs b/util/MySqlMigrations/Migrations/20250325231708_OrganizationIntegrations.cs new file mode 100644 index 0000000000..d488f3fc4a --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250325231708_OrganizationIntegrations.cs @@ -0,0 +1,89 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// <inheritdoc /> +public partial class OrganizationIntegrations : Migration +{ + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OrganizationIntegration", + columns: table => new + { + Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + OrganizationId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Type = table.Column<int>(type: "int", nullable: false), + Configuration = table.Column<string>(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + CreationDate = table.Column<DateTime>(type: "datetime(6)", nullable: false), + RevisionDate = table.Column<DateTime>(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationIntegration", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationIntegration_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "OrganizationIntegrationConfiguration", + columns: table => new + { + Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + OrganizationIntegrationId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + EventType = table.Column<int>(type: "int", nullable: false), + Configuration = table.Column<string>(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Template = table.Column<string>(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + CreationDate = table.Column<DateTime>(type: "datetime(6)", nullable: false), + RevisionDate = table.Column<DateTime>(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationIntegrationConfiguration", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationIntegrationConfiguration_OrganizationIntegration~", + column: x => x.OrganizationIntegrationId, + principalTable: "OrganizationIntegration", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationIntegration_OrganizationId", + table: "OrganizationIntegration", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationIntegration_OrganizationId_Type", + table: "OrganizationIntegration", + columns: new[] { "OrganizationId", "Type" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationIntegrationConfiguration_OrganizationIntegration~", + table: "OrganizationIntegrationConfiguration", + column: "OrganizationIntegrationId"); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrganizationIntegrationConfiguration"); + + migrationBuilder.DropTable( + name: "OrganizationIntegration"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index dfd5d4a983..4b3f8934f3 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -217,6 +217,68 @@ namespace Bit.MySqlMigrations.Migrations b.ToTable("Organization", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<string>("Configuration") + .HasColumnType("longtext"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<int>("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<Guid>("Id") + .HasColumnType("char(36)"); + + b.Property<string>("Configuration") + .HasColumnType("longtext"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property<int>("EventType") + .HasColumnType("int"); + + b.Property<Guid>("OrganizationIntegrationId") + .HasColumnType("char(36)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property<string>("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<Guid>("Id") @@ -407,10 +469,6 @@ namespace Bit.MySqlMigrations.Migrations b.Property<DateTime?>("AuthenticationDate") .HasColumnType("datetime(6)"); - b.Property<string>("RequestCountryName") - .HasMaxLength(200) - .HasColumnType("varchar(200)"); - b.Property<DateTime>("CreationDate") .HasColumnType("datetime(6)"); @@ -426,6 +484,10 @@ namespace Bit.MySqlMigrations.Migrations b.Property<string>("PublicKey") .HasColumnType("longtext"); + b.Property<string>("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + b.Property<string>("RequestDeviceIdentifier") .HasMaxLength(50) .HasColumnType("varchar(50)"); @@ -2259,6 +2321,28 @@ namespace Bit.MySqlMigrations.Migrations 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") diff --git a/util/PostgresMigrations/Migrations/20250325231701_OrganizationIntegrations.Designer.cs b/util/PostgresMigrations/Migrations/20250325231701_OrganizationIntegrations.Designer.cs new file mode 100644 index 0000000000..3639a6449e --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250325231701_OrganizationIntegrations.Designer.cs @@ -0,0 +1,3107 @@ +// <auto-generated /> +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("20250325231701_OrganizationIntegrations")] + partial class OrganizationIntegrations + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<bool>("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property<string>("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<string>("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<string>("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<string>("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<string>("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property<string>("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<string>("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<bool>("Enabled") + .HasColumnType("boolean"); + + b.Property<DateTime?>("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<byte?>("Gateway") + .HasColumnType("smallint"); + + b.Property<string>("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<string>("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<string>("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property<string>("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<bool>("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property<bool>("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property<bool>("LimitItemDeletion") + .HasColumnType("boolean"); + + b.Property<int?>("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property<int?>("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property<int?>("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property<short?>("MaxCollections") + .HasColumnType("smallint"); + + b.Property<short?>("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<DateTime?>("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<byte>("PlanType") + .HasColumnType("smallint"); + + b.Property<string>("PrivateKey") + .HasColumnType("text"); + + b.Property<string>("PublicKey") + .HasColumnType("text"); + + b.Property<string>("ReferenceData") + .HasColumnType("text"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<int?>("Seats") + .HasColumnType("integer"); + + b.Property<bool>("SelfHost") + .HasColumnType("boolean"); + + b.Property<int?>("SmSeats") + .HasColumnType("integer"); + + b.Property<int?>("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property<byte>("Status") + .HasColumnType("smallint"); + + b.Property<long?>("Storage") + .HasColumnType("bigint"); + + b.Property<string>("TwoFactorProviders") + .HasColumnType("text"); + + b.Property<bool>("Use2fa") + .HasColumnType("boolean"); + + b.Property<bool>("UseApi") + .HasColumnType("boolean"); + + b.Property<bool>("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property<bool>("UseDirectory") + .HasColumnType("boolean"); + + b.Property<bool>("UseEvents") + .HasColumnType("boolean"); + + b.Property<bool>("UseGroups") + .HasColumnType("boolean"); + + b.Property<bool>("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property<bool>("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property<bool>("UsePolicies") + .HasColumnType("boolean"); + + b.Property<bool>("UseResetPassword") + .HasColumnType("boolean"); + + b.Property<bool>("UseRiskInsights") + .HasColumnType("boolean"); + + b.Property<bool>("UseScim") + .HasColumnType("boolean"); + + b.Property<bool>("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property<bool>("UseSso") + .HasColumnType("boolean"); + + b.Property<bool>("UseTotp") + .HasColumnType("boolean"); + + b.Property<bool>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<string>("Configuration") + .HasColumnType("text"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<int>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<string>("Configuration") + .HasColumnType("text"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<int>("EventType") + .HasColumnType("integer"); + + b.Property<Guid>("OrganizationIntegrationId") + .HasColumnType("uuid"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Data") + .HasColumnType("text"); + + b.Property<bool>("Enabled") + .HasColumnType("boolean"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<byte>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<string>("BillingEmail") + .HasColumnType("text"); + + b.Property<string>("BillingPhone") + .HasColumnType("text"); + + b.Property<string>("BusinessAddress1") + .HasColumnType("text"); + + b.Property<string>("BusinessAddress2") + .HasColumnType("text"); + + b.Property<string>("BusinessAddress3") + .HasColumnType("text"); + + b.Property<string>("BusinessCountry") + .HasColumnType("text"); + + b.Property<string>("BusinessName") + .HasColumnType("text"); + + b.Property<string>("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("DiscountId") + .HasColumnType("text"); + + b.Property<bool>("Enabled") + .HasColumnType("boolean"); + + b.Property<byte?>("Gateway") + .HasColumnType("smallint"); + + b.Property<string>("GatewayCustomerId") + .HasColumnType("text"); + + b.Property<string>("GatewaySubscriptionId") + .HasColumnType("text"); + + b.Property<string>("Name") + .HasColumnType("text"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<byte>("Status") + .HasColumnType("smallint"); + + b.Property<byte>("Type") + .HasColumnType("smallint"); + + b.Property<bool>("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Key") + .HasColumnType("text"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<Guid>("ProviderId") + .HasColumnType("uuid"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Email") + .HasColumnType("text"); + + b.Property<string>("Key") + .HasColumnType("text"); + + b.Property<string>("Permissions") + .HasColumnType("text"); + + b.Property<Guid>("ProviderId") + .HasColumnType("uuid"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<byte>("Status") + .HasColumnType("smallint"); + + b.Property<byte>("Type") + .HasColumnType("smallint"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<string>("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property<bool?>("Approved") + .HasColumnType("boolean"); + + b.Property<DateTime?>("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Key") + .HasColumnType("text"); + + b.Property<string>("MasterPasswordHash") + .HasColumnType("text"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<string>("PublicKey") + .HasColumnType("text"); + + b.Property<string>("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property<string>("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<byte>("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property<string>("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<DateTime?>("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid?>("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property<byte>("Type") + .HasColumnType("smallint"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<Guid?>("GranteeId") + .HasColumnType("uuid"); + + b.Property<Guid>("GrantorId") + .HasColumnType("uuid"); + + b.Property<string>("KeyEncrypted") + .HasColumnType("text"); + + b.Property<DateTime?>("LastNotificationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<DateTime?>("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<byte>("Status") + .HasColumnType("smallint"); + + b.Property<byte>("Type") + .HasColumnType("smallint"); + + b.Property<int>("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<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property<DateTime?>("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property<string>("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property<DateTime?>("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property<string>("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<string>("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property<string>("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<long>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Data") + .HasColumnType("text"); + + b.Property<bool>("Enabled") + .HasColumnType("boolean"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<DateTime>("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<long>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("ExternalId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<Guid>("AaGuid") + .HasColumnType("uuid"); + + b.Property<int>("Counter") + .HasColumnType("integer"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<string>("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property<string>("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property<string>("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property<string>("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<string>("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<bool>("SupportsPrf") + .HasColumnType("boolean"); + + b.Property<string>("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<DateTime?>("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<string>("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<int?>("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property<short?>("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<byte>("PlanType") + .HasColumnType("smallint"); + + b.Property<Guid>("ProviderId") + .HasColumnType("uuid"); + + b.Property<int>("Seats") + .HasColumnType("integer"); + + b.Property<byte>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("InstallationId") + .HasColumnType("uuid"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<DateTime?>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<int>("AssignedSeats") + .HasColumnType("integer"); + + b.Property<Guid?>("ClientId") + .HasColumnType("uuid"); + + b.Property<string>("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<string>("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<string>("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<Guid>("ProviderId") + .HasColumnType("uuid"); + + b.Property<decimal>("Total") + .HasColumnType("numeric"); + + b.Property<int>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<int?>("AllocatedSeats") + .HasColumnType("integer"); + + b.Property<byte>("PlanType") + .HasColumnType("smallint"); + + b.Property<Guid>("ProviderId") + .HasColumnType("uuid"); + + b.Property<int?>("PurchasedSeats") + .HasColumnType("integer"); + + b.Property<int?>("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<string>("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property<DateTime?>("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property<DateTime>("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property<long?>("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property<byte[]>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<DateTime>("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<Guid>("CollectionId") + .HasColumnType("uuid"); + + b.Property<Guid>("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<Guid>("CollectionId") + .HasColumnType("uuid"); + + b.Property<Guid>("GroupId") + .HasColumnType("uuid"); + + b.Property<bool>("HidePasswords") + .HasColumnType("boolean"); + + b.Property<bool>("Manage") + .HasColumnType("boolean"); + + b.Property<bool>("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property<Guid>("CollectionId") + .HasColumnType("uuid"); + + b.Property<Guid>("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property<bool>("HidePasswords") + .HasColumnType("boolean"); + + b.Property<bool>("Manage") + .HasColumnType("boolean"); + + b.Property<bool>("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<bool>("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property<string>("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property<string>("EncryptedUserKey") + .HasColumnType("text"); + + b.Property<string>("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<string>("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<byte>("Type") + .HasColumnType("smallint"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<Guid?>("ActingUserId") + .HasColumnType("uuid"); + + b.Property<Guid?>("CipherId") + .HasColumnType("uuid"); + + b.Property<Guid?>("CollectionId") + .HasColumnType("uuid"); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp with time zone"); + + b.Property<byte?>("DeviceType") + .HasColumnType("smallint"); + + b.Property<string>("DomainName") + .HasColumnType("text"); + + b.Property<Guid?>("GroupId") + .HasColumnType("uuid"); + + b.Property<Guid?>("InstallationId") + .HasColumnType("uuid"); + + b.Property<string>("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<Guid?>("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property<Guid?>("PolicyId") + .HasColumnType("uuid"); + + b.Property<Guid?>("ProviderId") + .HasColumnType("uuid"); + + b.Property<Guid?>("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property<Guid?>("ProviderUserId") + .HasColumnType("uuid"); + + b.Property<Guid?>("SecretId") + .HasColumnType("uuid"); + + b.Property<Guid?>("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property<byte?>("SystemUser") + .HasColumnType("smallint"); + + b.Property<int>("Type") + .HasColumnType("integer"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<DateTime>("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<Guid>("GroupId") + .HasColumnType("uuid"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<string>("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<byte>("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<string>("Config") + .HasColumnType("text"); + + b.Property<bool>("Enabled") + .HasColumnType("boolean"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<byte>("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property<int>("JobRunCount") + .HasColumnType("integer"); + + b.Property<DateTime?>("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property<DateTime>("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<string>("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property<DateTime?>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<string>("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<DateTime?>("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<byte?>("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property<Guid?>("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property<Guid?>("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property<Guid>("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property<bool>("ToDelete") + .HasColumnType("boolean"); + + b.Property<DateTime?>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<bool>("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<string>("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property<string>("Key") + .HasColumnType("text"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<string>("Permissions") + .HasColumnType("text"); + + b.Property<string>("ResetPasswordKey") + .HasColumnType("text"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<short>("Status") + .HasColumnType("smallint"); + + b.Property<byte>("Type") + .HasColumnType("smallint"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<int>("AccessCount") + .HasColumnType("integer"); + + b.Property<Guid?>("CipherId") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Data") + .HasColumnType("text"); + + b.Property<DateTime>("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<bool>("Disabled") + .HasColumnType("boolean"); + + b.Property<DateTime?>("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<bool?>("HideEmail") + .HasColumnType("boolean"); + + b.Property<string>("Key") + .HasColumnType("text"); + + b.Property<int?>("MaxAccessCount") + .HasColumnType("integer"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<string>("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<byte>("Type") + .HasColumnType("smallint"); + + b.Property<Guid?>("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<string>("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property<bool>("Active") + .HasColumnType("boolean"); + + b.Property<string>("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<string>("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property<decimal>("Rate") + .HasColumnType("numeric"); + + b.Property<string>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<decimal>("Amount") + .HasColumnType("numeric"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<byte?>("Gateway") + .HasColumnType("smallint"); + + b.Property<string>("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<byte?>("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property<Guid?>("ProviderId") + .HasColumnType("uuid"); + + b.Property<bool?>("Refunded") + .HasColumnType("boolean"); + + b.Property<decimal?>("RefundedAmount") + .HasColumnType("numeric"); + + b.Property<byte>("Type") + .HasColumnType("smallint"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<DateTime>("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property<string>("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property<string>("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property<bool>("EmailVerified") + .HasColumnType("boolean"); + + b.Property<string>("EquivalentDomains") + .HasColumnType("text"); + + b.Property<string>("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property<int>("FailedLoginCount") + .HasColumnType("integer"); + + b.Property<bool>("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property<byte?>("Gateway") + .HasColumnType("smallint"); + + b.Property<string>("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<string>("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<byte>("Kdf") + .HasColumnType("smallint"); + + b.Property<int>("KdfIterations") + .HasColumnType("integer"); + + b.Property<int?>("KdfMemory") + .HasColumnType("integer"); + + b.Property<int?>("KdfParallelism") + .HasColumnType("integer"); + + b.Property<string>("Key") + .HasColumnType("text"); + + b.Property<DateTime?>("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property<DateTime?>("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property<DateTime?>("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property<DateTime?>("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<DateTime?>("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<string>("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property<string>("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<short?>("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property<string>("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<bool>("Premium") + .HasColumnType("boolean"); + + b.Property<DateTime?>("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("PrivateKey") + .HasColumnType("text"); + + b.Property<string>("PublicKey") + .HasColumnType("text"); + + b.Property<string>("ReferenceData") + .HasColumnType("text"); + + b.Property<DateTime?>("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<long?>("Storage") + .HasColumnType("bigint"); + + b.Property<string>("TwoFactorProviders") + .HasColumnType("text"); + + b.Property<string>("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property<bool>("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property<bool>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<string>("Body") + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property<byte>("ClientType") + .HasColumnType("smallint"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<bool>("Global") + .HasColumnType("boolean"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<byte>("Priority") + .HasColumnType("smallint"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid?>("TaskId") + .HasColumnType("uuid"); + + b.Property<string>("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<Guid?>("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<Guid>("UserId") + .HasColumnType("uuid"); + + b.Property<Guid>("NotificationId") + .HasColumnType("uuid"); + + b.Property<DateTime?>("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property<DateTime?>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<bool>("Enabled") + .HasColumnType("boolean"); + + b.Property<string>("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property<DateTime?>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property<bool>("Read") + .HasColumnType("boolean"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<bool>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<string>("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property<DateTime?>("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<DateTime?>("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Name") + .HasColumnType("text"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<DateTime>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<DateTime?>("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Key") + .HasColumnType("text"); + + b.Property<string>("Note") + .HasColumnType("text"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Name") + .HasColumnType("text"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<DateTime>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<string>("Attachments") + .HasColumnType("text"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Data") + .HasColumnType("text"); + + b.Property<DateTime?>("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Favorites") + .HasColumnType("text"); + + b.Property<string>("Folders") + .HasColumnType("text"); + + b.Property<string>("Key") + .HasColumnType("text"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<byte?>("Reprompt") + .HasColumnType("smallint"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<byte>("Type") + .HasColumnType("smallint"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Name") + .HasColumnType("text"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<Guid?>("CipherId") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<byte>("Status") + .HasColumnType("smallint"); + + b.Property<byte>("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<Guid>("ProjectsId") + .HasColumnType("uuid"); + + b.Property<Guid>("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<Guid?>("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property<Guid?>("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<Guid?>("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property<Guid?>("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<Guid?>("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property<Guid?>("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<Guid?>("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property<Guid?>("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<Guid?>("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property<Guid?>("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<Guid?>("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property<Guid?>("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<Guid?>("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property<Guid?>("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<Guid?>("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property<Guid?>("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"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20250325231701_OrganizationIntegrations.cs b/util/PostgresMigrations/Migrations/20250325231701_OrganizationIntegrations.cs new file mode 100644 index 0000000000..72b8572abf --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250325231701_OrganizationIntegrations.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// <inheritdoc /> +public partial class OrganizationIntegrations : Migration +{ + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OrganizationIntegration", + columns: table => new + { + Id = table.Column<Guid>(type: "uuid", nullable: false), + OrganizationId = table.Column<Guid>(type: "uuid", nullable: false), + Type = table.Column<int>(type: "integer", nullable: false), + Configuration = table.Column<string>(type: "text", nullable: true), + CreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), + RevisionDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationIntegration", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationIntegration_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "OrganizationIntegrationConfiguration", + columns: table => new + { + Id = table.Column<Guid>(type: "uuid", nullable: false), + OrganizationIntegrationId = table.Column<Guid>(type: "uuid", nullable: false), + EventType = table.Column<int>(type: "integer", nullable: false), + Configuration = table.Column<string>(type: "text", nullable: true), + Template = table.Column<string>(type: "text", nullable: true), + CreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), + RevisionDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationIntegrationConfiguration", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationIntegrationConfiguration_OrganizationIntegratio~", + column: x => x.OrganizationIntegrationId, + principalTable: "OrganizationIntegration", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationIntegration_OrganizationId", + table: "OrganizationIntegration", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationIntegration_OrganizationId_Type", + table: "OrganizationIntegration", + columns: new[] { "OrganizationId", "Type" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationIntegrationConfiguration_OrganizationIntegratio~", + table: "OrganizationIntegrationConfiguration", + column: "OrganizationIntegrationId"); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrganizationIntegrationConfiguration"); + + migrationBuilder.DropTable( + name: "OrganizationIntegration"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index a54bc6bddf..ebb8fa470f 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -220,6 +220,68 @@ namespace Bit.PostgresMigrations.Migrations b.ToTable("Organization", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<string>("Configuration") + .HasColumnType("text"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("uuid"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<int>("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<Guid>("Id") + .HasColumnType("uuid"); + + b.Property<string>("Configuration") + .HasColumnType("text"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property<int>("EventType") + .HasColumnType("integer"); + + b.Property<Guid>("OrganizationIntegrationId") + .HasColumnType("uuid"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("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<Guid>("Id") @@ -410,10 +472,6 @@ namespace Bit.PostgresMigrations.Migrations b.Property<DateTime?>("AuthenticationDate") .HasColumnType("timestamp with time zone"); - b.Property<string>("RequestCountryName") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - b.Property<DateTime>("CreationDate") .HasColumnType("timestamp with time zone"); @@ -429,6 +487,10 @@ namespace Bit.PostgresMigrations.Migrations b.Property<string>("PublicKey") .HasColumnType("text"); + b.Property<string>("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + b.Property<string>("RequestDeviceIdentifier") .HasMaxLength(50) .HasColumnType("character varying(50)"); @@ -2265,6 +2327,28 @@ namespace Bit.PostgresMigrations.Migrations 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") diff --git a/util/SqliteMigrations/Migrations/20250325231714_OrganizationIntegrations.Designer.cs b/util/SqliteMigrations/Migrations/20250325231714_OrganizationIntegrations.Designer.cs new file mode 100644 index 0000000000..279dd87331 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250325231714_OrganizationIntegrations.Designer.cs @@ -0,0 +1,3090 @@ +// <auto-generated /> +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("20250325231714_OrganizationIntegrations")] + partial class OrganizationIntegrations + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<bool>("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property<string>("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<string>("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<string>("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<string>("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<string>("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property<string>("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<string>("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<bool>("Enabled") + .HasColumnType("INTEGER"); + + b.Property<DateTime?>("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property<byte?>("Gateway") + .HasColumnType("INTEGER"); + + b.Property<string>("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<string>("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<string>("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<string>("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property<bool>("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property<bool>("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property<bool>("LimitItemDeletion") + .HasColumnType("INTEGER"); + + b.Property<int?>("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property<int?>("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property<int?>("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property<short?>("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property<short?>("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<DateTime?>("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property<string>("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<byte>("PlanType") + .HasColumnType("INTEGER"); + + b.Property<string>("PrivateKey") + .HasColumnType("TEXT"); + + b.Property<string>("PublicKey") + .HasColumnType("TEXT"); + + b.Property<string>("ReferenceData") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<int?>("Seats") + .HasColumnType("INTEGER"); + + b.Property<bool>("SelfHost") + .HasColumnType("INTEGER"); + + b.Property<int?>("SmSeats") + .HasColumnType("INTEGER"); + + b.Property<int?>("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property<byte>("Status") + .HasColumnType("INTEGER"); + + b.Property<long?>("Storage") + .HasColumnType("INTEGER"); + + b.Property<string>("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property<bool>("Use2fa") + .HasColumnType("INTEGER"); + + b.Property<bool>("UseApi") + .HasColumnType("INTEGER"); + + b.Property<bool>("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property<bool>("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property<bool>("UseEvents") + .HasColumnType("INTEGER"); + + b.Property<bool>("UseGroups") + .HasColumnType("INTEGER"); + + b.Property<bool>("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property<bool>("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property<bool>("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property<bool>("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property<bool>("UseRiskInsights") + .HasColumnType("INTEGER"); + + b.Property<bool>("UseScim") + .HasColumnType("INTEGER"); + + b.Property<bool>("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property<bool>("UseSso") + .HasColumnType("INTEGER"); + + b.Property<bool>("UseTotp") + .HasColumnType("INTEGER"); + + b.Property<bool>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<string>("Configuration") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<int>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<string>("Configuration") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<int>("EventType") + .HasColumnType("INTEGER"); + + b.Property<Guid>("OrganizationIntegrationId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<string>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("Data") + .HasColumnType("TEXT"); + + b.Property<bool>("Enabled") + .HasColumnType("INTEGER"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<byte>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<string>("BillingEmail") + .HasColumnType("TEXT"); + + b.Property<string>("BillingPhone") + .HasColumnType("TEXT"); + + b.Property<string>("BusinessAddress1") + .HasColumnType("TEXT"); + + b.Property<string>("BusinessAddress2") + .HasColumnType("TEXT"); + + b.Property<string>("BusinessAddress3") + .HasColumnType("TEXT"); + + b.Property<string>("BusinessCountry") + .HasColumnType("TEXT"); + + b.Property<string>("BusinessName") + .HasColumnType("TEXT"); + + b.Property<string>("BusinessTaxNumber") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("DiscountId") + .HasColumnType("TEXT"); + + b.Property<bool>("Enabled") + .HasColumnType("INTEGER"); + + b.Property<byte?>("Gateway") + .HasColumnType("INTEGER"); + + b.Property<string>("GatewayCustomerId") + .HasColumnType("TEXT"); + + b.Property<string>("GatewaySubscriptionId") + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<byte>("Status") + .HasColumnType("INTEGER"); + + b.Property<byte>("Type") + .HasColumnType("INTEGER"); + + b.Property<bool>("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("Key") + .HasColumnType("TEXT"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<Guid>("ProviderId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<string>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("Email") + .HasColumnType("TEXT"); + + b.Property<string>("Key") + .HasColumnType("TEXT"); + + b.Property<string>("Permissions") + .HasColumnType("TEXT"); + + b.Property<Guid>("ProviderId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<byte>("Status") + .HasColumnType("INTEGER"); + + b.Property<byte>("Type") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<string>("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property<bool?>("Approved") + .HasColumnType("INTEGER"); + + b.Property<DateTime?>("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("Key") + .HasColumnType("TEXT"); + + b.Property<string>("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<string>("PublicKey") + .HasColumnType("TEXT"); + + b.Property<string>("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property<string>("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<byte>("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property<string>("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<DateTime?>("ResponseDate") + .HasColumnType("TEXT"); + + b.Property<Guid?>("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property<byte>("Type") + .HasColumnType("INTEGER"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<Guid?>("GranteeId") + .HasColumnType("TEXT"); + + b.Property<Guid>("GrantorId") + .HasColumnType("TEXT"); + + b.Property<string>("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<byte>("Status") + .HasColumnType("INTEGER"); + + b.Property<byte>("Type") + .HasColumnType("INTEGER"); + + b.Property<int>("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<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property<DateTime?>("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<string>("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property<DateTime?>("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property<string>("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property<string>("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property<string>("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property<string>("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<long>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("Data") + .HasColumnType("TEXT"); + + b.Property<bool>("Enabled") + .HasColumnType("INTEGER"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("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<long>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("ExternalId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<Guid>("AaGuid") + .HasColumnType("TEXT"); + + b.Property<int>("Counter") + .HasColumnType("INTEGER"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<string>("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property<string>("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property<string>("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<string>("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<bool>("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property<string>("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property<string>("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<string>("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<int?>("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property<short?>("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<byte>("PlanType") + .HasColumnType("INTEGER"); + + b.Property<Guid>("ProviderId") + .HasColumnType("TEXT"); + + b.Property<int>("Seats") + .HasColumnType("INTEGER"); + + b.Property<byte>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<Guid>("InstallationId") + .HasColumnType("TEXT"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<int>("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("ClientId") + .HasColumnType("TEXT"); + + b.Property<string>("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<DateTime>("Created") + .HasColumnType("TEXT"); + + b.Property<string>("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<string>("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<string>("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<Guid>("ProviderId") + .HasColumnType("TEXT"); + + b.Property<decimal>("Total") + .HasColumnType("TEXT"); + + b.Property<int>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<int?>("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property<byte>("PlanType") + .HasColumnType("INTEGER"); + + b.Property<Guid>("ProviderId") + .HasColumnType("TEXT"); + + b.Property<int?>("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property<int?>("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<string>("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property<DateTime?>("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property<DateTime>("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property<long?>("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property<byte[]>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property<Guid>("CollectionId") + .HasColumnType("TEXT"); + + b.Property<Guid>("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<Guid>("CollectionId") + .HasColumnType("TEXT"); + + b.Property<Guid>("GroupId") + .HasColumnType("TEXT"); + + b.Property<bool>("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property<bool>("Manage") + .HasColumnType("INTEGER"); + + b.Property<bool>("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property<Guid>("CollectionId") + .HasColumnType("TEXT"); + + b.Property<Guid>("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property<bool>("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property<bool>("Manage") + .HasColumnType("INTEGER"); + + b.Property<bool>("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property<bool>("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property<string>("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property<string>("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property<string>("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<string>("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<byte>("Type") + .HasColumnType("INTEGER"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<Guid?>("ActingUserId") + .HasColumnType("TEXT"); + + b.Property<Guid?>("CipherId") + .HasColumnType("TEXT"); + + b.Property<Guid?>("CollectionId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("Date") + .HasColumnType("TEXT"); + + b.Property<byte?>("DeviceType") + .HasColumnType("INTEGER"); + + b.Property<string>("DomainName") + .HasColumnType("TEXT"); + + b.Property<Guid?>("GroupId") + .HasColumnType("TEXT"); + + b.Property<Guid?>("InstallationId") + .HasColumnType("TEXT"); + + b.Property<string>("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<Guid?>("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property<Guid?>("PolicyId") + .HasColumnType("TEXT"); + + b.Property<Guid?>("ProviderId") + .HasColumnType("TEXT"); + + b.Property<Guid?>("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property<Guid?>("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property<Guid?>("SecretId") + .HasColumnType("TEXT"); + + b.Property<Guid?>("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property<byte?>("SystemUser") + .HasColumnType("INTEGER"); + + b.Property<int>("Type") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property<Guid>("GroupId") + .HasColumnType("TEXT"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<string>("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<byte>("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<string>("Config") + .HasColumnType("TEXT"); + + b.Property<bool>("Enabled") + .HasColumnType("INTEGER"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<byte>("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<int>("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property<DateTime?>("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property<DateTime>("NextRunDate") + .HasColumnType("TEXT"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<string>("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<DateTime?>("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<string>("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<DateTime?>("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property<string>("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<byte?>("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property<Guid?>("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property<Guid>("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property<bool>("ToDelete") + .HasColumnType("INTEGER"); + + b.Property<DateTime?>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<bool>("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<string>("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property<string>("Key") + .HasColumnType("TEXT"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<string>("Permissions") + .HasColumnType("TEXT"); + + b.Property<string>("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<short>("Status") + .HasColumnType("INTEGER"); + + b.Property<byte>("Type") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<int>("AccessCount") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("CipherId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("Data") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DeletionDate") + .HasColumnType("TEXT"); + + b.Property<bool>("Disabled") + .HasColumnType("INTEGER"); + + b.Property<DateTime?>("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property<bool?>("HideEmail") + .HasColumnType("INTEGER"); + + b.Property<string>("Key") + .HasColumnType("TEXT"); + + b.Property<int?>("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<string>("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<byte>("Type") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("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<string>("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property<bool>("Active") + .HasColumnType("INTEGER"); + + b.Property<string>("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<string>("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property<decimal>("Rate") + .HasColumnType("TEXT"); + + b.Property<string>("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<decimal>("Amount") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property<byte?>("Gateway") + .HasColumnType("INTEGER"); + + b.Property<string>("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<byte?>("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("ProviderId") + .HasColumnType("TEXT"); + + b.Property<bool?>("Refunded") + .HasColumnType("INTEGER"); + + b.Property<decimal?>("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property<byte>("Type") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<DateTime>("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property<string>("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property<string>("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property<string>("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<bool>("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property<string>("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property<string>("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property<int>("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property<bool>("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property<byte?>("Gateway") + .HasColumnType("INTEGER"); + + b.Property<string>("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<string>("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<byte>("Kdf") + .HasColumnType("INTEGER"); + + b.Property<int>("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property<int?>("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property<int?>("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property<string>("Key") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property<string>("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property<string>("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property<string>("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<short?>("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property<string>("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<bool>("Premium") + .HasColumnType("INTEGER"); + + b.Property<DateTime?>("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property<string>("PrivateKey") + .HasColumnType("TEXT"); + + b.Property<string>("PublicKey") + .HasColumnType("TEXT"); + + b.Property<string>("ReferenceData") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<string>("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property<long?>("Storage") + .HasColumnType("INTEGER"); + + b.Property<string>("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property<string>("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<bool>("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property<bool>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<string>("Body") + .HasMaxLength(3000) + .HasColumnType("TEXT"); + + b.Property<byte>("ClientType") + .HasColumnType("INTEGER"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<bool>("Global") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<byte>("Priority") + .HasColumnType("INTEGER"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<Guid?>("TaskId") + .HasColumnType("TEXT"); + + b.Property<string>("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<Guid?>("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<Guid>("UserId") + .HasColumnType("TEXT"); + + b.Property<Guid>("NotificationId") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("DeletedDate") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<bool>("Enabled") + .HasColumnType("INTEGER"); + + b.Property<string>("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property<DateTime?>("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property<bool>("Read") + .HasColumnType("INTEGER"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<bool>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<string>("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property<DateTime?>("ExpireAt") + .HasColumnType("TEXT"); + + b.Property<string>("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<string>("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("DeletedDate") + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .HasColumnType("TEXT"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("DeletedDate") + .HasColumnType("TEXT"); + + b.Property<string>("Key") + .HasColumnType("TEXT"); + + b.Property<string>("Note") + .HasColumnType("TEXT"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<string>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .HasColumnType("TEXT"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<string>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<string>("Attachments") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("Data") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("DeletedDate") + .HasColumnType("TEXT"); + + b.Property<string>("Favorites") + .HasColumnType("TEXT"); + + b.Property<string>("Folders") + .HasColumnType("TEXT"); + + b.Property<string>("Key") + .HasColumnType("TEXT"); + + b.Property<Guid?>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<byte?>("Reprompt") + .HasColumnType("INTEGER"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<byte>("Type") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<Guid>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<Guid?>("CipherId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<byte>("Status") + .HasColumnType("INTEGER"); + + b.Property<byte>("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<Guid>("ProjectsId") + .HasColumnType("TEXT"); + + b.Property<Guid>("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<Guid?>("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property<Guid?>("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<Guid?>("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property<Guid?>("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<Guid?>("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property<Guid?>("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<Guid?>("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property<Guid?>("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<Guid?>("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property<Guid?>("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<Guid?>("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property<Guid?>("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<Guid?>("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property<Guid?>("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<Guid?>("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property<Guid?>("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"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/SqliteMigrations/Migrations/20250325231714_OrganizationIntegrations.cs b/util/SqliteMigrations/Migrations/20250325231714_OrganizationIntegrations.cs new file mode 100644 index 0000000000..53550ea3e2 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250325231714_OrganizationIntegrations.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// <inheritdoc /> +public partial class OrganizationIntegrations : Migration +{ + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OrganizationIntegration", + columns: table => new + { + Id = table.Column<Guid>(type: "TEXT", nullable: false), + OrganizationId = table.Column<Guid>(type: "TEXT", nullable: false), + Type = table.Column<int>(type: "INTEGER", nullable: false), + Configuration = table.Column<string>(type: "TEXT", nullable: true), + CreationDate = table.Column<DateTime>(type: "TEXT", nullable: false), + RevisionDate = table.Column<DateTime>(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationIntegration", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationIntegration_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "OrganizationIntegrationConfiguration", + columns: table => new + { + Id = table.Column<Guid>(type: "TEXT", nullable: false), + OrganizationIntegrationId = table.Column<Guid>(type: "TEXT", nullable: false), + EventType = table.Column<int>(type: "INTEGER", nullable: false), + Configuration = table.Column<string>(type: "TEXT", nullable: true), + Template = table.Column<string>(type: "TEXT", nullable: true), + CreationDate = table.Column<DateTime>(type: "TEXT", nullable: false), + RevisionDate = table.Column<DateTime>(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationIntegrationConfiguration", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationIntegrationConfiguration_OrganizationIntegration_OrganizationIntegrationId", + column: x => x.OrganizationIntegrationId, + principalTable: "OrganizationIntegration", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationIntegration_OrganizationId", + table: "OrganizationIntegration", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationIntegration_OrganizationId_Type", + table: "OrganizationIntegration", + columns: new[] { "OrganizationId", "Type" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationIntegrationConfiguration_OrganizationIntegrationId", + table: "OrganizationIntegrationConfiguration", + column: "OrganizationIntegrationId"); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrganizationIntegrationConfiguration"); + + migrationBuilder.DropTable( + name: "OrganizationIntegration"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 824f2ffec5..753c049651 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -212,6 +212,68 @@ namespace Bit.SqliteMigrations.Migrations b.ToTable("Organization", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<string>("Configuration") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<Guid>("OrganizationId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<int>("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<Guid>("Id") + .HasColumnType("TEXT"); + + b.Property<string>("Configuration") + .HasColumnType("TEXT"); + + b.Property<DateTime>("CreationDate") + .HasColumnType("TEXT"); + + b.Property<int>("EventType") + .HasColumnType("INTEGER"); + + b.Property<Guid>("OrganizationIntegrationId") + .HasColumnType("TEXT"); + + b.Property<DateTime>("RevisionDate") + .HasColumnType("TEXT"); + + b.Property<string>("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<Guid>("Id") @@ -402,10 +464,6 @@ namespace Bit.SqliteMigrations.Migrations b.Property<DateTime?>("AuthenticationDate") .HasColumnType("TEXT"); - b.Property<string>("RequestCountryName") - .HasMaxLength(200) - .HasColumnType("TEXT"); - b.Property<DateTime>("CreationDate") .HasColumnType("TEXT"); @@ -421,6 +479,10 @@ namespace Bit.SqliteMigrations.Migrations b.Property<string>("PublicKey") .HasColumnType("TEXT"); + b.Property<string>("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + b.Property<string>("RequestDeviceIdentifier") .HasMaxLength(50) .HasColumnType("TEXT"); @@ -2248,6 +2310,28 @@ namespace Bit.SqliteMigrations.Migrations 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") From 6178bb2db183614edfd300acb0da0c5f8171e40e Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 26 Mar 2025 13:08:19 -0500 Subject: [PATCH 19/19] only create security tasks when a task doesn't exist for the submitted cipher (#5558) --- .../Controllers/SecurityTaskController.cs | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/Api/Vault/Controllers/SecurityTaskController.cs b/src/Api/Vault/Controllers/SecurityTaskController.cs index 2693d60825..2fe1025ba7 100644 --- a/src/Api/Vault/Controllers/SecurityTaskController.cs +++ b/src/Api/Vault/Controllers/SecurityTaskController.cs @@ -5,6 +5,7 @@ using Bit.Core; using Bit.Core.Services; using Bit.Core.Utilities; using Bit.Core.Vault.Commands.Interfaces; +using Bit.Core.Vault.Entities; using Bit.Core.Vault.Enums; using Bit.Core.Vault.Queries; using Microsoft.AspNetCore.Authorization; @@ -89,11 +90,28 @@ public class SecurityTaskController : Controller public async Task<ListResponseModel<SecurityTasksResponseModel>> BulkCreateTasks(Guid orgId, [FromBody] BulkCreateSecurityTasksRequestModel model) { - var securityTasks = await _createManyTasksCommand.CreateAsync(orgId, model.Tasks); + // Retrieve existing pending security tasks for the organization + var pendingSecurityTasks = await _getTasksForOrganizationQuery.GetTasksAsync(orgId, SecurityTaskStatus.Pending); - await _createManyTaskNotificationsCommand.CreateAsync(orgId, securityTasks); + // Get the security tasks that are already associated with a cipher within the submitted model + var existingTasks = pendingSecurityTasks.Where(x => model.Tasks.Any(y => y.CipherId == x.CipherId)).ToList(); - var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList(); + // Get tasks that need to be created + var tasksToCreateFromModel = model.Tasks.Where(x => !existingTasks.Any(y => y.CipherId == x.CipherId)).ToList(); + + ICollection<SecurityTask> newSecurityTasks = new List<SecurityTask>(); + + if (tasksToCreateFromModel.Count != 0) + { + newSecurityTasks = await _createManyTasksCommand.CreateAsync(orgId, tasksToCreateFromModel); + } + + // Combine existing tasks and newly created tasks + var allTasks = existingTasks.Concat(newSecurityTasks); + + await _createManyTaskNotificationsCommand.CreateAsync(orgId, allTasks); + + var response = allTasks.Select(x => new SecurityTasksResponseModel(x)).ToList(); return new ListResponseModel<SecurityTasksResponseModel>(response); } }