From 5227ee7d9052641d27fcb59afea9637217aa7ed7 Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Tue, 19 Nov 2024 17:19:22 -0500 Subject: [PATCH 01/94] [PM-13746] Remove loggedInUserId parameter. (#5033) 1. Remove _organizationService.ValidateOrganizationUserUpdatePermissions since it is not needed for updating group associations. 2. Remove loggedInUserId since it's no longer needed. 3. Update/remove related tests. --- .../Public/Controllers/MembersController.cs | 2 +- .../IUpdateOrganizationUserGroupsCommand.cs | 2 +- .../UpdateOrganizationUserGroupsCommand.cs | 9 +------ ...pdateOrganizationUserGroupsCommandTests.cs | 25 ++----------------- 4 files changed, 5 insertions(+), 33 deletions(-) diff --git a/src/Api/AdminConsole/Public/Controllers/MembersController.cs b/src/Api/AdminConsole/Public/Controllers/MembersController.cs index 4e99353d45..76bd29d38e 100644 --- a/src/Api/AdminConsole/Public/Controllers/MembersController.cs +++ b/src/Api/AdminConsole/Public/Controllers/MembersController.cs @@ -213,7 +213,7 @@ public class MembersController : Controller { return new NotFoundResult(); } - await _updateOrganizationUserGroupsCommand.UpdateUserGroupsAsync(existingUser, model.GroupIds, null); + await _updateOrganizationUserGroupsCommand.UpdateUserGroupsAsync(existingUser, model.GroupIds); return new OkResult(); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IUpdateOrganizationUserGroupsCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IUpdateOrganizationUserGroupsCommand.cs index 530b3839e6..9e8037e68c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IUpdateOrganizationUserGroupsCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IUpdateOrganizationUserGroupsCommand.cs @@ -4,5 +4,5 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interface public interface IUpdateOrganizationUserGroupsCommand { - Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable groupIds, Guid? loggedInUserId); + Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable groupIds); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserGroupsCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserGroupsCommand.cs index 0331f2ed59..615a33dbf4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserGroupsCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserGroupsCommand.cs @@ -9,25 +9,18 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; public class UpdateOrganizationUserGroupsCommand : IUpdateOrganizationUserGroupsCommand { private readonly IEventService _eventService; - private readonly IOrganizationService _organizationService; private readonly IOrganizationUserRepository _organizationUserRepository; public UpdateOrganizationUserGroupsCommand( IEventService eventService, - IOrganizationService organizationService, IOrganizationUserRepository organizationUserRepository) { _eventService = eventService; - _organizationService = organizationService; _organizationUserRepository = organizationUserRepository; } - public async Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable groupIds, Guid? loggedInUserId) + public async Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable groupIds) { - if (loggedInUserId.HasValue) - { - await _organizationService.ValidateOrganizationUserUpdatePermissions(organizationUser.OrganizationId, organizationUser.Type, null, organizationUser.GetPermissions()); - } await _organizationUserRepository.UpdateGroupsAsync(organizationUser.Id, groupIds); await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UpdatedGroups); } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserGroupsCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserGroupsCommandTests.cs index a581b463f2..ad598649a4 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserGroupsCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserGroupsCommandTests.cs @@ -14,34 +14,13 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers; public class UpdateOrganizationUserGroupsCommandTests { [Theory, BitAutoData] - public async Task UpdateUserGroups_Passes( + public async Task UpdateUserGroups_ShouldUpdateUserGroupsAndLogUserEvent( OrganizationUser organizationUser, IEnumerable groupIds, SutProvider sutProvider) { - await sutProvider.Sut.UpdateUserGroupsAsync(organizationUser, groupIds, null); + await sutProvider.Sut.UpdateUserGroupsAsync(organizationUser, groupIds); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .ValidateOrganizationUserUpdatePermissions(default, default, default, default); - await sutProvider.GetDependency().Received(1) - .UpdateGroupsAsync(organizationUser.Id, groupIds); - await sutProvider.GetDependency().Received(1) - .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UpdatedGroups); - } - - [Theory, BitAutoData] - public async Task UpdateUserGroups_WithSavingUserId_Passes( - OrganizationUser organizationUser, - IEnumerable groupIds, - Guid savingUserId, - SutProvider sutProvider) - { - organizationUser.Permissions = null; - - await sutProvider.Sut.UpdateUserGroupsAsync(organizationUser, groupIds, savingUserId); - - await sutProvider.GetDependency().Received(1) - .ValidateOrganizationUserUpdatePermissions(organizationUser.OrganizationId, organizationUser.Type, null, organizationUser.GetPermissions()); await sutProvider.GetDependency().Received(1) .UpdateGroupsAsync(organizationUser.Id, groupIds); await sutProvider.GetDependency().Received(1) From 77cde50ee1ffe43a5ba2517e9f4e107eeb405009 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:32:53 -0500 Subject: [PATCH 02/94] Updated customer metadata when updating to use bank account (#5050) --- src/Core/Billing/Services/Implementations/SubscriberService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index 401d9ce2ca..b0d290a556 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -768,8 +768,9 @@ public class SubscriberService( { var metadata = customer.Metadata ?? new Dictionary(); - if (metadata.ContainsKey(BraintreeCustomerIdKey)) + if (metadata.TryGetValue(BraintreeCustomerIdKey, out var value)) { + metadata[BraintreeCustomerIdOldKey] = value; metadata[BraintreeCustomerIdKey] = null; await stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions From eb20adb53eb703feacb36d38c6451f91c4e89c4c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 20 Nov 2024 10:24:29 -0800 Subject: [PATCH 03/94] Add QA flag (#5005) Co-authored-by: Matt Bishop Co-authored-by: aj-bw <81774843+aj-bw@users.noreply.github.com> --- src/Api/Vault/Controllers/SyncController.cs | 2 +- src/Core/Constants.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index 5ffaa0e34c..c08a5f86e0 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -112,7 +112,7 @@ public class SyncController : Controller private ICollection FilterSSHKeys(ICollection ciphers) { - if (_currentContext.ClientVersion >= _sshKeyCipherMinimumVersion) + if (_currentContext.ClientVersion >= _sshKeyCipherMinimumVersion || _featureService.IsEnabled(FeatureFlagKeys.SSHVersionCheckQAOverride)) { return ciphers; } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 3de1060e1f..8fe3886539 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -123,6 +123,7 @@ public static class FeatureFlagKeys 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"; From 052235bed63d53e2d7f8e7f8b45f1e7f0cc14955 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:36:50 -0500 Subject: [PATCH 04/94] [PM-15048] Update bank account verification to use descriptor code (#5048) * Update verify bank account process to use descriptor code * Run dotnet format --- .../OrganizationBillingController.cs | 7 ++- .../Requests/VerifyBankAccountRequestBody.cs | 6 +- src/Core/Billing/BillingException.cs | 4 +- src/Core/Billing/Constants/StripeConstants.cs | 3 + .../Billing/Services/ISubscriberService.cs | 6 +- .../Implementations/SubscriberService.cs | 55 ++++++++++++------- .../Services/SubscriberServiceTests.cs | 13 ++--- 7 files changed, 56 insertions(+), 38 deletions(-) diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index b6a26f2404..7da0a0f602 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -206,6 +206,11 @@ public class OrganizationBillingController( return Error.Unauthorized(); } + if (requestBody.DescriptorCode.Length != 6 || !requestBody.DescriptorCode.StartsWith("SM")) + { + return Error.BadRequest("Statement descriptor should be a 6-character value that starts with 'SM'"); + } + var organization = await organizationRepository.GetByIdAsync(organizationId); if (organization == null) @@ -213,7 +218,7 @@ public class OrganizationBillingController( return Error.NotFound(); } - await subscriberService.VerifyBankAccount(organization, (requestBody.Amount1, requestBody.Amount2)); + await subscriberService.VerifyBankAccount(organization, requestBody.DescriptorCode); return TypedResults.Ok(); } diff --git a/src/Api/Billing/Models/Requests/VerifyBankAccountRequestBody.cs b/src/Api/Billing/Models/Requests/VerifyBankAccountRequestBody.cs index de98755f30..3e97d07a90 100644 --- a/src/Api/Billing/Models/Requests/VerifyBankAccountRequestBody.cs +++ b/src/Api/Billing/Models/Requests/VerifyBankAccountRequestBody.cs @@ -4,8 +4,6 @@ namespace Bit.Api.Billing.Models.Requests; public class VerifyBankAccountRequestBody { - [Range(0, 99)] - public long Amount1 { get; set; } - [Range(0, 99)] - public long Amount2 { get; set; } + [Required] + public string DescriptorCode { get; set; } } diff --git a/src/Core/Billing/BillingException.cs b/src/Core/Billing/BillingException.cs index cdb3ce6b5a..c2b1b9f457 100644 --- a/src/Core/Billing/BillingException.cs +++ b/src/Core/Billing/BillingException.cs @@ -5,5 +5,7 @@ public class BillingException( string message = null, Exception innerException = null) : Exception(message, innerException) { - public string Response { get; } = response ?? "Something went wrong with your request. Please contact support."; + public const string DefaultMessage = "Something went wrong with your request. Please contact support."; + + public string Response { get; } = response ?? DefaultMessage; } diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs index 44cda35b70..7371b8b7e9 100644 --- a/src/Core/Billing/Constants/StripeConstants.cs +++ b/src/Core/Billing/Constants/StripeConstants.cs @@ -25,6 +25,9 @@ public static class StripeConstants public static class ErrorCodes { public const string CustomerTaxLocationInvalid = "customer_tax_location_invalid"; + public const string PaymentMethodMicroDepositVerificationAttemptsExceeded = "payment_method_microdeposit_verification_attempts_exceeded"; + public const string PaymentMethodMicroDepositVerificationDescriptorCodeMismatch = "payment_method_microdeposit_verification_descriptor_code_mismatch"; + public const string PaymentMethodMicroDepositVerificationTimeout = "payment_method_microdeposit_verification_timeout"; public const string TaxIdInvalid = "tax_id_invalid"; } diff --git a/src/Core/Billing/Services/ISubscriberService.cs b/src/Core/Billing/Services/ISubscriberService.cs index e7decd1cb2..bb0a23020c 100644 --- a/src/Core/Billing/Services/ISubscriberService.cs +++ b/src/Core/Billing/Services/ISubscriberService.cs @@ -141,13 +141,13 @@ public interface ISubscriberService TaxInformation taxInformation); /// - /// Verifies the subscriber's pending bank account using the provided . + /// Verifies the subscriber's pending bank account using the provided . /// /// The subscriber to verify the bank account for. - /// Deposits made to the subscriber's bank account in order to ensure they have access to it. + /// The code attached to a deposit made to the subscriber's bank account in order to ensure they have access to it. /// Learn more. /// Task VerifyBankAccount( ISubscriber subscriber, - (long, long) microdeposits); + string descriptorCode); } diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index b0d290a556..9b8f64be82 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -3,6 +3,7 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; @@ -650,41 +651,53 @@ public class SubscriberService( public async Task VerifyBankAccount( ISubscriber subscriber, - (long, long) microdeposits) + string descriptorCode) { - ArgumentNullException.ThrowIfNull(subscriber); - var setupIntentId = await setupIntentCache.Get(subscriber.Id); if (string.IsNullOrEmpty(setupIntentId)) { logger.LogError("No setup intent ID exists to verify for subscriber with ID ({SubscriberID})", subscriber.Id); - throw new BillingException(); } - var (amount1, amount2) = microdeposits; - - await stripeAdapter.SetupIntentVerifyMicroDeposit(setupIntentId, new SetupIntentVerifyMicrodepositsOptions + try { - Amounts = [amount1, amount2] - }); + await stripeAdapter.SetupIntentVerifyMicroDeposit(setupIntentId, + new SetupIntentVerifyMicrodepositsOptions { DescriptorCode = descriptorCode }); - var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId); + var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId); - await stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId, new PaymentMethodAttachOptions - { - Customer = subscriber.GatewayCustomerId - }); + await stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId, + new PaymentMethodAttachOptions { Customer = subscriber.GatewayCustomerId }); - await stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, - new CustomerUpdateOptions - { - InvoiceSettings = new CustomerInvoiceSettingsOptions + await stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, + new CustomerUpdateOptions { - DefaultPaymentMethod = setupIntent.PaymentMethodId - } - }); + InvoiceSettings = new CustomerInvoiceSettingsOptions + { + DefaultPaymentMethod = setupIntent.PaymentMethodId + } + }); + } + catch (StripeException stripeException) + { + if (!string.IsNullOrEmpty(stripeException.StripeError?.Code)) + { + var message = stripeException.StripeError.Code switch + { + StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationAttemptsExceeded => "You have exceeded the number of allowed verification attempts. Please contact support.", + StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationDescriptorCodeMismatch => "The verification code you provided does not match the one sent to your bank account. Please try again.", + StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationTimeout => "Your bank account was not verified within the required time period. Please contact support.", + _ => BillingException.DefaultMessage + }; + + throw new BadRequestException(message); + } + + logger.LogError(stripeException, "An unhandled Stripe exception was thrown while verifying subscriber's ({SubscriberID}) bank account", subscriber.Id); + throw new BillingException(); + } } #region Shared Utilities diff --git a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs index 652c22764f..385b185ffe 100644 --- a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs +++ b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs @@ -1581,21 +1581,18 @@ public class SubscriberServiceTests #region VerifyBankAccount - [Theory, BitAutoData] - public async Task VerifyBankAccount_NullSubscriber_ThrowsArgumentNullException( - SutProvider sutProvider) => await Assert.ThrowsAsync( - () => sutProvider.Sut.VerifyBankAccount(null, (0, 0))); - [Theory, BitAutoData] public async Task VerifyBankAccount_NoSetupIntentId_ThrowsBillingException( Provider provider, - SutProvider sutProvider) => await ThrowsBillingExceptionAsync(() => sutProvider.Sut.VerifyBankAccount(provider, (1, 1))); + SutProvider sutProvider) => await ThrowsBillingExceptionAsync(() => sutProvider.Sut.VerifyBankAccount(provider, "")); [Theory, BitAutoData] public async Task VerifyBankAccount_MakesCorrectInvocations( Provider provider, SutProvider sutProvider) { + const string descriptorCode = "SM1234"; + var setupIntent = new SetupIntent { Id = "setup_intent_id", @@ -1608,11 +1605,11 @@ public class SubscriberServiceTests stripeAdapter.SetupIntentGet(setupIntent.Id).Returns(setupIntent); - await sutProvider.Sut.VerifyBankAccount(provider, (1, 1)); + await sutProvider.Sut.VerifyBankAccount(provider, descriptorCode); await stripeAdapter.Received(1).SetupIntentVerifyMicroDeposit(setupIntent.Id, Arg.Is( - options => options.Amounts[0] == 1 && options.Amounts[1] == 1)); + options => options.DescriptorCode == descriptorCode)); await stripeAdapter.Received(1).PaymentMethodAttachAsync(setupIntent.PaymentMethodId, Arg.Is( From 92b94fd4ee3290a764f5b839761c303cf3260c1b Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Wed, 20 Nov 2024 14:18:05 -0600 Subject: [PATCH 05/94] PM-15066 added drop feature and unit tests. (#5053) --- .../Tools/Controllers/ReportsController.cs | 28 ++++- ...dPasswordHealthReportApplicationCommand.cs | 2 +- ...pPasswordHealthReportApplicationCommand.cs | 31 ++++++ ...dPasswordHealthReportApplicationCommand.cs | 2 +- ...pPasswordHealthReportApplicationCommand.cs | 9 ++ .../ReportingServiceCollectionExtensions.cs | 1 + ...dPasswordHealthReportApplicationRequest.cs | 2 +- ...pPasswordHealthReportApplicationRequest.cs | 7 ++ .../Controllers/ReportsControllerTests.cs | 97 +++++++++++++++- ...wordHealthReportApplicationCommandTests.cs | 2 +- ...wordHealthReportApplicationCommandTests.cs | 104 ++++++++++++++++++ 11 files changed, 278 insertions(+), 7 deletions(-) create mode 100644 src/Core/Tools/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs create mode 100644 src/Core/Tools/ReportFeatures/Interfaces/IDropPasswordHealthReportApplicationCommand.cs rename src/Core/Tools/{ => ReportFeatures}/Requests/AddPasswordHealthReportApplicationRequest.cs (72%) create mode 100644 src/Core/Tools/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs create mode 100644 test/Core.Test/Tools/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs diff --git a/src/Api/Tools/Controllers/ReportsController.cs b/src/Api/Tools/Controllers/ReportsController.cs index 9f465c7b8c..4c0a802da2 100644 --- a/src/Api/Tools/Controllers/ReportsController.cs +++ b/src/Api/Tools/Controllers/ReportsController.cs @@ -7,7 +7,6 @@ using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.ReportFeatures.Interfaces; using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces; using Bit.Core.Tools.ReportFeatures.Requests; -using Bit.Core.Tools.Requests; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -21,18 +20,21 @@ public class ReportsController : Controller private readonly IMemberAccessCipherDetailsQuery _memberAccessCipherDetailsQuery; private readonly IAddPasswordHealthReportApplicationCommand _addPwdHealthReportAppCommand; private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery; + private readonly IDropPasswordHealthReportApplicationCommand _dropPwdHealthReportAppCommand; public ReportsController( ICurrentContext currentContext, IMemberAccessCipherDetailsQuery memberAccessCipherDetailsQuery, IAddPasswordHealthReportApplicationCommand addPasswordHealthReportApplicationCommand, - IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery + IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery, + IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand ) { _currentContext = currentContext; _memberAccessCipherDetailsQuery = memberAccessCipherDetailsQuery; _addPwdHealthReportAppCommand = addPasswordHealthReportApplicationCommand; _getPwdHealthReportAppQuery = getPasswordHealthReportApplicationQuery; + _dropPwdHealthReportAppCommand = dropPwdHealthReportAppCommand; } /// @@ -161,4 +163,26 @@ public class ReportsController : Controller return await _addPwdHealthReportAppCommand.AddPasswordHealthReportApplicationAsync(commandRequests); } + + /// + /// Drops a record from PasswordHealthReportApplication + /// + /// + /// A single instance of DropPasswordHealthReportApplicationRequest + /// { OrganizationId, array of PasswordHealthReportApplicationIds } + /// + /// + /// If user does not have access to the organization + /// If the organization does not have any records + [HttpDelete("password-health-report-application")] + public async Task DropPasswordHealthReportApplication( + [FromBody] DropPasswordHealthReportApplicationRequest request) + { + if (!await _currentContext.AccessReports(request.OrganizationId)) + { + throw new NotFoundException(); + } + + await _dropPwdHealthReportAppCommand.DropPasswordHealthReportApplicationAsync(request); + } } diff --git a/src/Core/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs b/src/Core/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs index c6bdb44179..b191799ba0 100644 --- a/src/Core/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs +++ b/src/Core/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs @@ -2,8 +2,8 @@ using Bit.Core.Repositories; using Bit.Core.Tools.Entities; using Bit.Core.Tools.ReportFeatures.Interfaces; +using Bit.Core.Tools.ReportFeatures.Requests; using Bit.Core.Tools.Repositories; -using Bit.Core.Tools.Requests; namespace Bit.Core.Tools.ReportFeatures; diff --git a/src/Core/Tools/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs b/src/Core/Tools/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs new file mode 100644 index 0000000000..73a8f84e6a --- /dev/null +++ b/src/Core/Tools/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs @@ -0,0 +1,31 @@ +using Bit.Core.Exceptions; +using Bit.Core.Tools.ReportFeatures.Interfaces; +using Bit.Core.Tools.ReportFeatures.Requests; +using Bit.Core.Tools.Repositories; + +namespace Bit.Core.Tools.ReportFeatures; + +public class DropPasswordHealthReportApplicationCommand : IDropPasswordHealthReportApplicationCommand +{ + private IPasswordHealthReportApplicationRepository _passwordHealthReportApplicationRepo; + + public DropPasswordHealthReportApplicationCommand( + IPasswordHealthReportApplicationRepository passwordHealthReportApplicationRepository) + { + _passwordHealthReportApplicationRepo = passwordHealthReportApplicationRepository; + } + + public async Task DropPasswordHealthReportApplicationAsync(DropPasswordHealthReportApplicationRequest request) + { + var data = await _passwordHealthReportApplicationRepo.GetByOrganizationIdAsync(request.OrganizationId); + if (data == null) + { + throw new BadRequestException("Organization does not have any records."); + } + + data.Where(_ => request.PasswordHealthReportApplicationIds.Contains(_.Id)).ToList().ForEach(async _ => + { + await _passwordHealthReportApplicationRepo.DeleteAsync(_); + }); + } +} diff --git a/src/Core/Tools/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs b/src/Core/Tools/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs index 86e7d44c77..9d145a79b6 100644 --- a/src/Core/Tools/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs +++ b/src/Core/Tools/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs @@ -1,5 +1,5 @@ using Bit.Core.Tools.Entities; -using Bit.Core.Tools.Requests; +using Bit.Core.Tools.ReportFeatures.Requests; namespace Bit.Core.Tools.ReportFeatures.Interfaces; diff --git a/src/Core/Tools/ReportFeatures/Interfaces/IDropPasswordHealthReportApplicationCommand.cs b/src/Core/Tools/ReportFeatures/Interfaces/IDropPasswordHealthReportApplicationCommand.cs new file mode 100644 index 0000000000..0adf09cab8 --- /dev/null +++ b/src/Core/Tools/ReportFeatures/Interfaces/IDropPasswordHealthReportApplicationCommand.cs @@ -0,0 +1,9 @@ +using Bit.Core.Tools.ReportFeatures.Requests; + +namespace Bit.Core.Tools.ReportFeatures.Interfaces; + +public interface IDropPasswordHealthReportApplicationCommand +{ + Task DropPasswordHealthReportApplicationAsync(DropPasswordHealthReportApplicationRequest request); +} + diff --git a/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs b/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs index 85e5388a0b..4970f0515b 100644 --- a/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs +++ b/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs @@ -11,5 +11,6 @@ public static class ReportingServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Core/Tools/Requests/AddPasswordHealthReportApplicationRequest.cs b/src/Core/Tools/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs similarity index 72% rename from src/Core/Tools/Requests/AddPasswordHealthReportApplicationRequest.cs rename to src/Core/Tools/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs index f5a1d35eae..dfc544b1c3 100644 --- a/src/Core/Tools/Requests/AddPasswordHealthReportApplicationRequest.cs +++ b/src/Core/Tools/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Tools.Requests; +namespace Bit.Core.Tools.ReportFeatures.Requests; public class AddPasswordHealthReportApplicationRequest { diff --git a/src/Core/Tools/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs b/src/Core/Tools/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs new file mode 100644 index 0000000000..1464e68f04 --- /dev/null +++ b/src/Core/Tools/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Tools.ReportFeatures.Requests; + +public class DropPasswordHealthReportApplicationRequest +{ + public Guid OrganizationId { get; set; } + public IEnumerable PasswordHealthReportApplicationIds { get; set; } +} diff --git a/test/Api.Test/Tools/Controllers/ReportsControllerTests.cs b/test/Api.Test/Tools/Controllers/ReportsControllerTests.cs index 07d37f672c..3057e10641 100644 --- a/test/Api.Test/Tools/Controllers/ReportsControllerTests.cs +++ b/test/Api.Test/Tools/Controllers/ReportsControllerTests.cs @@ -1,7 +1,9 @@ -using Bit.Api.Tools.Controllers; +using AutoFixture; +using Bit.Api.Tools.Controllers; using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Tools.ReportFeatures.Interfaces; +using Bit.Core.Tools.ReportFeatures.Requests; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -45,5 +47,98 @@ public class ReportsControllerTests .Received(0); } + [Theory, BitAutoData] + public async Task AddPasswordHealthReportApplicationAsync_withAccess_success(SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(true); + // Act + var request = new Api.Tools.Models.PasswordHealthReportApplicationModel + { + OrganizationId = Guid.NewGuid(), + Url = "https://example.com", + }; + await sutProvider.Sut.AddPasswordHealthReportApplication(request); + + // Assert + _ = sutProvider.GetDependency() + .Received(1) + .AddPasswordHealthReportApplicationAsync(Arg.Is(_ => + _.OrganizationId == request.OrganizationId && _.Url == request.Url)); + } + + [Theory, BitAutoData] + public async Task AddPasswordHealthReportApplicationAsync_multiple_withAccess_success( + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(true); + + // Act + var fixture = new Fixture(); + var request = fixture.CreateMany(2); + await sutProvider.Sut.AddPasswordHealthReportApplications(request); + + // Assert + _ = sutProvider.GetDependency() + .Received(1) + .AddPasswordHealthReportApplicationAsync(Arg.Any>()); + } + + [Theory, BitAutoData] + public async Task AddPasswordHealthReportApplicationAsync_withoutAccess(SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(false); + + // Act + var request = new Api.Tools.Models.PasswordHealthReportApplicationModel + { + OrganizationId = Guid.NewGuid(), + Url = "https://example.com", + }; + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.AddPasswordHealthReportApplication(request)); + + // Assert + _ = sutProvider.GetDependency() + .Received(0); + } + + [Theory, BitAutoData] + public async Task DropPasswordHealthReportApplicationAsync_withoutAccess(SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(false); + + // Act + var fixture = new Fixture(); + var request = fixture.Create(); + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.AddPasswordHealthReportApplication(request)); + + // Assert + _ = sutProvider.GetDependency() + .Received(0); + } + + [Theory, BitAutoData] + public async Task DropPasswordHealthReportApplicationAsync_withAccess_success(SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(true); + + // Act + var fixture = new Fixture(); + var request = fixture.Create(); + await sutProvider.Sut.DropPasswordHealthReportApplication(request); + + // Assert + _ = sutProvider.GetDependency() + .Received(1) + .DropPasswordHealthReportApplicationAsync(Arg.Is(_ => + _.OrganizationId == request.OrganizationId && + _.PasswordHealthReportApplicationIds == request.PasswordHealthReportApplicationIds)); + } } diff --git a/test/Core.Test/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs b/test/Core.Test/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs index 8c3a68fdcb..5018123e22 100644 --- a/test/Core.Test/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs +++ b/test/Core.Test/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs @@ -4,8 +4,8 @@ using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Tools.Entities; using Bit.Core.Tools.ReportFeatures; +using Bit.Core.Tools.ReportFeatures.Requests; using Bit.Core.Tools.Repositories; -using Bit.Core.Tools.Requests; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; diff --git a/test/Core.Test/Tools/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs b/test/Core.Test/Tools/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs new file mode 100644 index 0000000000..c459d0e81b --- /dev/null +++ b/test/Core.Test/Tools/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs @@ -0,0 +1,104 @@ +using AutoFixture; +using Bit.Core.Exceptions; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.ReportFeatures; +using Bit.Core.Tools.ReportFeatures.Requests; +using Bit.Core.Tools.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Tools.ReportFeatures; + +[SutProviderCustomize] +public class DeletePasswordHealthReportApplicationCommandTests +{ + [Theory, BitAutoData] + public async Task DropPasswordHealthReportApplicationAsync_withValidRequest_Success( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var passwordHealthReportApplications = fixture.CreateMany(2).ToList(); + // only take one id from the list - we only want to drop one record + var request = fixture.Build() + .With(x => x.PasswordHealthReportApplicationIds, + passwordHealthReportApplications.Select(x => x.Id).Take(1).ToList()) + .Create(); + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(Arg.Any()) + .Returns(passwordHealthReportApplications); + + // Act + await sutProvider.Sut.DropPasswordHealthReportApplicationAsync(request); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .GetByOrganizationIdAsync(request.OrganizationId); + + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(Arg.Is(_ => + request.PasswordHealthReportApplicationIds.Contains(_.Id))); + } + + [Theory, BitAutoData] + public async Task DropPasswordHealthReportApplicationAsync_withValidRequest_nothingToDrop( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var passwordHealthReportApplications = fixture.CreateMany(2).ToList(); + // we are passing invalid data + var request = fixture.Build() + .With(x => x.PasswordHealthReportApplicationIds, new List { Guid.NewGuid() }) + .Create(); + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(Arg.Any()) + .Returns(passwordHealthReportApplications); + + // Act + await sutProvider.Sut.DropPasswordHealthReportApplicationAsync(request); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .GetByOrganizationIdAsync(request.OrganizationId); + + await sutProvider.GetDependency() + .Received(0) + .DeleteAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task DropPasswordHealthReportApplicationAsync_withNodata_fails( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + // we are passing invalid data + var request = fixture.Build() + .Create(); + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(Arg.Any()) + .Returns(null as List); + + // Act + await Assert.ThrowsAsync(() => + sutProvider.Sut.DropPasswordHealthReportApplicationAsync(request)); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .GetByOrganizationIdAsync(request.OrganizationId); + + await sutProvider.GetDependency() + .Received(0) + .DeleteAsync(Arg.Any()); + } +} From fae8692d2ab4f2b39ef8a5cbee0801b71b04b4a6 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 21 Nov 2024 10:17:04 -0800 Subject: [PATCH 06/94] [PM-12607] Move key rotation & validators to km ownership (#4941) * Move key rotation & validators to km ownership * Fix build errors * Fix build errors * Fix import ordering * Update validator namespace * Move key rotation data to km ownership * Fix linting * Fix namespaces * Fix namespace * Fix namespaces * Move rotateuserkeycommandtests to km ownership --- src/Api/Auth/Controllers/AccountsController.cs | 5 +++-- .../Validators/CipherRotationValidator.cs | 5 ++--- .../Validators/EmergencyAccessRotationValidator.cs | 2 +- .../Validators/FolderRotationValidator.cs | 5 ++--- .../Validators/IRotationValidator.cs | 2 +- .../Validators/OrganizationUserRotationValidator.cs | 3 +-- .../Validators/SendRotationValidator.cs | 5 ++--- .../Validators/WebAuthnLoginKeyRotationValidator.cs | 2 +- src/Api/Startup.cs | 5 +---- .../Repositories/IOrganizationUserRepository.cs | 2 +- src/Core/Auth/Repositories/IEmergencyAccessRepository.cs | 2 +- src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs | 2 +- .../Auth/UserFeatures/UserServiceCollectionExtensions.cs | 4 ++-- .../Models/Data/RotateUserKeyData.cs | 3 ++- .../UserKey/IRotateUserKeyCommand.cs | 6 +++--- .../UserKey/Implementations/RotateUserKeyCommand.cs | 6 +++--- src/Core/Repositories/IUserRepository.cs | 4 ++-- src/Core/Tools/Repositories/ISendRepository.cs | 2 +- src/Core/Vault/Repositories/ICipherRepository.cs | 4 ++-- src/Core/Vault/Repositories/IFolderRepository.cs | 2 +- .../AdminConsole/Repositories/OrganizationUserRepository.cs | 2 +- .../Auth/Repositories/EmergencyAccessRepository.cs | 2 +- .../Auth/Repositories/WebAuthnCredentialRepository.cs | 2 +- src/Infrastructure.Dapper/Repositories/UserRepository.cs | 2 +- .../Tools/Repositories/SendRepository.cs | 2 +- .../Vault/Repositories/CipherRepository.cs | 2 +- .../Vault/Repositories/FolderRepository.cs | 2 +- .../AdminConsole/Repositories/OrganizationUserRepository.cs | 2 +- .../Auth/Repositories/EmergencyAccessRepository.cs | 2 +- .../Auth/Repositories/WebAuthnCredentialRepository.cs | 2 +- .../Repositories/UserRepository.cs | 2 +- .../Tools/Repositories/SendRepository.cs | 2 +- .../Vault/Repositories/CipherRepository.cs | 2 +- .../Vault/Repositories/FolderRepository.cs | 2 +- test/Api.Test/Auth/Controllers/AccountsControllerTests.cs | 4 ++-- .../Validators/CipherRotationValidatorTests.cs | 6 +++--- .../Validators/EmergencyAccessRotationValidatorTests.cs | 4 ++-- .../Validators/FolderRotationValidatorTests.cs | 6 +++--- .../Validators/OrganizationUserRotationValidatorTests.cs | 4 ++-- .../Validators/SendRotationValidatorTests.cs | 4 ++-- .../Validators/WebauthnLoginKeyRotationValidatorTests.cs | 4 ++-- .../UserKey/RotateUserKeyCommandTests.cs | 6 +++--- 42 files changed, 66 insertions(+), 71 deletions(-) rename src/Api/{Vault => KeyManagement}/Validators/CipherRotationValidator.cs (92%) rename src/Api/{Auth => KeyManagement}/Validators/EmergencyAccessRotationValidator.cs (97%) rename src/Api/{Vault => KeyManagement}/Validators/FolderRotationValidator.cs (91%) rename src/Api/{Auth => KeyManagement}/Validators/IRotationValidator.cs (94%) rename src/Api/{AdminConsole => KeyManagement}/Validators/OrganizationUserRotationValidator.cs (96%) rename src/Api/{Tools => KeyManagement}/Validators/SendRotationValidator.cs (94%) rename src/Api/{Auth => KeyManagement}/Validators/WebAuthnLoginKeyRotationValidator.cs (97%) rename src/Core/{Auth => KeyManagement}/Models/Data/RotateUserKeyData.cs (89%) rename src/Core/{Auth/UserFeatures => KeyManagement}/UserKey/IRotateUserKeyCommand.cs (91%) rename src/Core/{Auth/UserFeatures => KeyManagement}/UserKey/Implementations/RotateUserKeyCommand.cs (97%) rename test/Api.Test/{Vault => KeyManagement}/Validators/CipherRotationValidatorTests.cs (94%) rename test/Api.Test/{Auth => KeyManagement}/Validators/EmergencyAccessRotationValidatorTests.cs (98%) rename test/Api.Test/{Vault => KeyManagement}/Validators/FolderRotationValidatorTests.cs (94%) rename test/Api.Test/{AdminConsole => KeyManagement}/Validators/OrganizationUserRotationValidatorTests.cs (98%) rename test/Api.Test/{Tools => KeyManagement}/Validators/SendRotationValidatorTests.cs (98%) rename test/Api.Test/{Auth => KeyManagement}/Validators/WebauthnLoginKeyRotationValidatorTests.cs (97%) rename test/Core.Test/{Auth/UserFeatures => KeyManagement}/UserKey/RotateUserKeyCommandTests.cs (95%) diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 193077dc15..a94e170cbb 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -3,7 +3,7 @@ using Bit.Api.AdminConsole.Models.Response; using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Auth.Models.Request.WebAuthn; -using Bit.Api.Auth.Validators; +using Bit.Api.KeyManagement.Validators; using Bit.Api.Models.Request; using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Response; @@ -18,7 +18,6 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; -using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Billing.Models; using Bit.Core.Billing.Services; @@ -26,6 +25,8 @@ using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Api.Response; using Bit.Core.Models.Business; using Bit.Core.Repositories; diff --git a/src/Api/Vault/Validators/CipherRotationValidator.cs b/src/Api/KeyManagement/Validators/CipherRotationValidator.cs similarity index 92% rename from src/Api/Vault/Validators/CipherRotationValidator.cs rename to src/Api/KeyManagement/Validators/CipherRotationValidator.cs index 77e437017a..ab56db4195 100644 --- a/src/Api/Vault/Validators/CipherRotationValidator.cs +++ b/src/Api/KeyManagement/Validators/CipherRotationValidator.cs @@ -1,11 +1,10 @@ -using Bit.Api.Auth.Validators; -using Bit.Api.Vault.Models.Request; +using Bit.Api.Vault.Models.Request; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Repositories; -namespace Bit.Api.Vault.Validators; +namespace Bit.Api.KeyManagement.Validators; public class CipherRotationValidator : IRotationValidator, IEnumerable> { diff --git a/src/Api/Auth/Validators/EmergencyAccessRotationValidator.cs b/src/Api/KeyManagement/Validators/EmergencyAccessRotationValidator.cs similarity index 97% rename from src/Api/Auth/Validators/EmergencyAccessRotationValidator.cs rename to src/Api/KeyManagement/Validators/EmergencyAccessRotationValidator.cs index 5a038730e3..3fd9273e4e 100644 --- a/src/Api/Auth/Validators/EmergencyAccessRotationValidator.cs +++ b/src/Api/KeyManagement/Validators/EmergencyAccessRotationValidator.cs @@ -5,7 +5,7 @@ using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; -namespace Bit.Api.Auth.Validators; +namespace Bit.Api.KeyManagement.Validators; public class EmergencyAccessRotationValidator : IRotationValidator, IEnumerable> diff --git a/src/Api/Vault/Validators/FolderRotationValidator.cs b/src/Api/KeyManagement/Validators/FolderRotationValidator.cs similarity index 91% rename from src/Api/Vault/Validators/FolderRotationValidator.cs rename to src/Api/KeyManagement/Validators/FolderRotationValidator.cs index 4290c08b13..add0a46c1c 100644 --- a/src/Api/Vault/Validators/FolderRotationValidator.cs +++ b/src/Api/KeyManagement/Validators/FolderRotationValidator.cs @@ -1,11 +1,10 @@ -using Bit.Api.Auth.Validators; -using Bit.Api.Vault.Models.Request; +using Bit.Api.Vault.Models.Request; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Repositories; -namespace Bit.Api.Vault.Validators; +namespace Bit.Api.KeyManagement.Validators; public class FolderRotationValidator : IRotationValidator, IEnumerable> { diff --git a/src/Api/Auth/Validators/IRotationValidator.cs b/src/Api/KeyManagement/Validators/IRotationValidator.cs similarity index 94% rename from src/Api/Auth/Validators/IRotationValidator.cs rename to src/Api/KeyManagement/Validators/IRotationValidator.cs index fb6534ebee..50f4dd0043 100644 --- a/src/Api/Auth/Validators/IRotationValidator.cs +++ b/src/Api/KeyManagement/Validators/IRotationValidator.cs @@ -1,7 +1,7 @@ using Bit.Core.Entities; using Bit.Core.Exceptions; -namespace Bit.Api.Auth.Validators; +namespace Bit.Api.KeyManagement.Validators; /// /// A consistent interface for domains to validate re-encrypted data before saved to database. Some examples are:
diff --git a/src/Api/AdminConsole/Validators/OrganizationUserRotationValidator.cs b/src/Api/KeyManagement/Validators/OrganizationUserRotationValidator.cs similarity index 96% rename from src/Api/AdminConsole/Validators/OrganizationUserRotationValidator.cs rename to src/Api/KeyManagement/Validators/OrganizationUserRotationValidator.cs index c9cf39ae04..5023521fe3 100644 --- a/src/Api/AdminConsole/Validators/OrganizationUserRotationValidator.cs +++ b/src/Api/KeyManagement/Validators/OrganizationUserRotationValidator.cs @@ -1,10 +1,9 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; -using Bit.Api.Auth.Validators; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Repositories; -namespace Bit.Api.AdminConsole.Validators; +namespace Bit.Api.KeyManagement.Validators; /// /// Organization user implementation for diff --git a/src/Api/Tools/Validators/SendRotationValidator.cs b/src/Api/KeyManagement/Validators/SendRotationValidator.cs similarity index 94% rename from src/Api/Tools/Validators/SendRotationValidator.cs rename to src/Api/KeyManagement/Validators/SendRotationValidator.cs index 74b36832ff..c39f563b51 100644 --- a/src/Api/Tools/Validators/SendRotationValidator.cs +++ b/src/Api/KeyManagement/Validators/SendRotationValidator.cs @@ -1,12 +1,11 @@ -using Bit.Api.Auth.Validators; -using Bit.Api.Tools.Models.Request; +using Bit.Api.Tools.Models.Request; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Repositories; using Bit.Core.Tools.Services; -namespace Bit.Api.Tools.Validators; +namespace Bit.Api.KeyManagement.Validators; /// /// Send implementation for diff --git a/src/Api/Auth/Validators/WebAuthnLoginKeyRotationValidator.cs b/src/Api/KeyManagement/Validators/WebAuthnLoginKeyRotationValidator.cs similarity index 97% rename from src/Api/Auth/Validators/WebAuthnLoginKeyRotationValidator.cs rename to src/Api/KeyManagement/Validators/WebAuthnLoginKeyRotationValidator.cs index 5c4d0ef302..1706aebd78 100644 --- a/src/Api/Auth/Validators/WebAuthnLoginKeyRotationValidator.cs +++ b/src/Api/KeyManagement/Validators/WebAuthnLoginKeyRotationValidator.cs @@ -4,7 +4,7 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Entities; using Bit.Core.Exceptions; -namespace Bit.Api.Auth.Validators; +namespace Bit.Api.KeyManagement.Validators; public class WebAuthnLoginKeyRotationValidator : IRotationValidator, IEnumerable> { diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 65935440c5..1adf3f67dc 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -8,13 +8,10 @@ using Bit.Core.Utilities; using IdentityModel; using System.Globalization; using Bit.Api.AdminConsole.Models.Request.Organizations; -using Bit.Api.AdminConsole.Validators; using Bit.Api.Auth.Models.Request; -using Bit.Api.Auth.Validators; +using Bit.Api.KeyManagement.Validators; using Bit.Api.Tools.Models.Request; -using Bit.Api.Tools.Validators; using Bit.Api.Vault.Models.Request; -using Bit.Api.Vault.Validators; using Bit.Core.Auth.Entities; using Bit.Core.IdentityServer; using Bit.SharedWeb.Health; diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs index a3a68b5de2..cb540c212b 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Enums; -using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; diff --git a/src/Core/Auth/Repositories/IEmergencyAccessRepository.cs b/src/Core/Auth/Repositories/IEmergencyAccessRepository.cs index 6edb941d32..63ec04106e 100644 --- a/src/Core/Auth/Repositories/IEmergencyAccessRepository.cs +++ b/src/Core/Auth/Repositories/IEmergencyAccessRepository.cs @@ -1,6 +1,6 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Data; -using Bit.Core.Auth.UserFeatures.UserKey; +using Bit.Core.KeyManagement.UserKey; #nullable enable diff --git a/src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs b/src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs index 9a7fc88207..29ed9d2210 100644 --- a/src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs +++ b/src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs @@ -1,6 +1,6 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Data; -using Bit.Core.Auth.UserFeatures.UserKey; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; #nullable enable diff --git a/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs b/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs index 2469c124b3..df102c855f 100644 --- a/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs +++ b/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs @@ -5,12 +5,12 @@ using Bit.Core.Auth.UserFeatures.Registration.Implementations; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; -using Bit.Core.Auth.UserFeatures.UserKey; -using Bit.Core.Auth.UserFeatures.UserKey.Implementations; using Bit.Core.Auth.UserFeatures.UserMasterPassword; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Auth.UserFeatures.WebAuthnLogin; using Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations; +using Bit.Core.KeyManagement.UserKey; +using Bit.Core.KeyManagement.UserKey.Implementations; using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Core/Auth/Models/Data/RotateUserKeyData.cs b/src/Core/KeyManagement/Models/Data/RotateUserKeyData.cs similarity index 89% rename from src/Core/Auth/Models/Data/RotateUserKeyData.cs rename to src/Core/KeyManagement/Models/Data/RotateUserKeyData.cs index f361c2a2cc..9813f760f3 100644 --- a/src/Core/Auth/Models/Data/RotateUserKeyData.cs +++ b/src/Core/KeyManagement/Models/Data/RotateUserKeyData.cs @@ -1,9 +1,10 @@ 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.Auth.Models.Data; +namespace Bit.Core.KeyManagement.Models.Data; public class RotateUserKeyData { diff --git a/src/Core/Auth/UserFeatures/UserKey/IRotateUserKeyCommand.cs b/src/Core/KeyManagement/UserKey/IRotateUserKeyCommand.cs similarity index 91% rename from src/Core/Auth/UserFeatures/UserKey/IRotateUserKeyCommand.cs rename to src/Core/KeyManagement/UserKey/IRotateUserKeyCommand.cs index cd2df59645..90dc90541f 100644 --- a/src/Core/Auth/UserFeatures/UserKey/IRotateUserKeyCommand.cs +++ b/src/Core/KeyManagement/UserKey/IRotateUserKeyCommand.cs @@ -1,9 +1,9 @@ -using Bit.Core.Auth.Models.Data; -using Bit.Core.Entities; +using Bit.Core.Entities; +using Bit.Core.KeyManagement.Models.Data; using Microsoft.AspNetCore.Identity; using Microsoft.Data.SqlClient; -namespace Bit.Core.Auth.UserFeatures.UserKey; +namespace Bit.Core.KeyManagement.UserKey; /// /// Responsible for rotation of a user key and updating database with re-encrypted data diff --git a/src/Core/Auth/UserFeatures/UserKey/Implementations/RotateUserKeyCommand.cs b/src/Core/KeyManagement/UserKey/Implementations/RotateUserKeyCommand.cs similarity index 97% rename from src/Core/Auth/UserFeatures/UserKey/Implementations/RotateUserKeyCommand.cs rename to src/Core/KeyManagement/UserKey/Implementations/RotateUserKeyCommand.cs index 4c7ca20737..68b2c60293 100644 --- a/src/Core/Auth/UserFeatures/UserKey/Implementations/RotateUserKeyCommand.cs +++ b/src/Core/KeyManagement/UserKey/Implementations/RotateUserKeyCommand.cs @@ -1,13 +1,13 @@ -using Bit.Core.Auth.Models.Data; -using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.Repositories; using Bit.Core.Entities; +using Bit.Core.KeyManagement.Models.Data; 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.Auth.UserFeatures.UserKey.Implementations; +namespace Bit.Core.KeyManagement.UserKey.Implementations; /// public class RotateUserKeyCommand : IRotateUserKeyCommand diff --git a/src/Core/Repositories/IUserRepository.cs b/src/Core/Repositories/IUserRepository.cs index b7c654f431..22e2ec1a07 100644 --- a/src/Core/Repositories/IUserRepository.cs +++ b/src/Core/Repositories/IUserRepository.cs @@ -1,5 +1,5 @@ -using Bit.Core.Auth.UserFeatures.UserKey; -using Bit.Core.Entities; +using Bit.Core.Entities; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Data; #nullable enable diff --git a/src/Core/Tools/Repositories/ISendRepository.cs b/src/Core/Tools/Repositories/ISendRepository.cs index 2cbcce1f92..6de89f0374 100644 --- a/src/Core/Tools/Repositories/ISendRepository.cs +++ b/src/Core/Tools/Repositories/ISendRepository.cs @@ -1,6 +1,6 @@ #nullable enable -using Bit.Core.Auth.UserFeatures.UserKey; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Core.Tools.Entities; diff --git a/src/Core/Vault/Repositories/ICipherRepository.cs b/src/Core/Vault/Repositories/ICipherRepository.cs index 132aa5ac60..f3f34c595b 100644 --- a/src/Core/Vault/Repositories/ICipherRepository.cs +++ b/src/Core/Vault/Repositories/ICipherRepository.cs @@ -1,5 +1,5 @@ -using Bit.Core.Auth.UserFeatures.UserKey; -using Bit.Core.Entities; +using Bit.Core.Entities; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; diff --git a/src/Core/Vault/Repositories/IFolderRepository.cs b/src/Core/Vault/Repositories/IFolderRepository.cs index f192437613..c4693b2a13 100644 --- a/src/Core/Vault/Repositories/IFolderRepository.cs +++ b/src/Core/Vault/Repositories/IFolderRepository.cs @@ -1,4 +1,4 @@ -using Bit.Core.Auth.UserFeatures.UserKey; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Core.Vault.Entities; diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs index 361b1f058c..d5bdd3b6a2 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -2,9 +2,9 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; -using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; diff --git a/src/Infrastructure.Dapper/Auth/Repositories/EmergencyAccessRepository.cs b/src/Infrastructure.Dapper/Auth/Repositories/EmergencyAccessRepository.cs index e6bf92bdea..4d597ab045 100644 --- a/src/Infrastructure.Dapper/Auth/Repositories/EmergencyAccessRepository.cs +++ b/src/Infrastructure.Dapper/Auth/Repositories/EmergencyAccessRepository.cs @@ -1,7 +1,7 @@ using System.Data; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Data; -using Bit.Core.Auth.UserFeatures.UserKey; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Infrastructure.Dapper.Auth.Helpers; diff --git a/src/Infrastructure.Dapper/Auth/Repositories/WebAuthnCredentialRepository.cs b/src/Infrastructure.Dapper/Auth/Repositories/WebAuthnCredentialRepository.cs index 0f7e1ea1b9..7dfcd15d49 100644 --- a/src/Infrastructure.Dapper/Auth/Repositories/WebAuthnCredentialRepository.cs +++ b/src/Infrastructure.Dapper/Auth/Repositories/WebAuthnCredentialRepository.cs @@ -2,7 +2,7 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; -using Bit.Core.Auth.UserFeatures.UserKey; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Infrastructure.Dapper.Repositories; diff --git a/src/Infrastructure.Dapper/Repositories/UserRepository.cs b/src/Infrastructure.Dapper/Repositories/UserRepository.cs index a96c986778..9e613fdf08 100644 --- a/src/Infrastructure.Dapper/Repositories/UserRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/UserRepository.cs @@ -1,8 +1,8 @@ using System.Data; using System.Text.Json; using Bit.Core; -using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Entities; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.Settings; diff --git a/src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs b/src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs index 12fbbd4eb6..81a94f0f7c 100644 --- a/src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs +++ b/src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs @@ -1,7 +1,7 @@ #nullable enable using System.Data; -using Bit.Core.Auth.UserFeatures.UserKey; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Settings; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Repositories; diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs index 697edb3f37..69b1383f4b 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs @@ -1,7 +1,7 @@ using System.Data; using System.Text.Json; -using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Entities; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Settings; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; diff --git a/src/Infrastructure.Dapper/Vault/Repositories/FolderRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/FolderRepository.cs index bf1548b24c..a6f6f2ee22 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/FolderRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/FolderRepository.cs @@ -1,5 +1,5 @@ using System.Data; -using Bit.Core.Auth.UserFeatures.UserKey; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Settings; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Repositories; diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index 0c9f1d0b9d..a64c19704d 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -1,7 +1,7 @@ using AutoMapper; using Bit.Core.AdminConsole.Enums; -using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Enums; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; diff --git a/src/Infrastructure.EntityFramework/Auth/Repositories/EmergencyAccessRepository.cs b/src/Infrastructure.EntityFramework/Auth/Repositories/EmergencyAccessRepository.cs index 22ca89fa0a..e1ea9bc03f 100644 --- a/src/Infrastructure.EntityFramework/Auth/Repositories/EmergencyAccessRepository.cs +++ b/src/Infrastructure.EntityFramework/Auth/Repositories/EmergencyAccessRepository.cs @@ -1,7 +1,7 @@ using AutoMapper; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; -using Bit.Core.Auth.UserFeatures.UserKey; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.Auth.Models; using Bit.Infrastructure.EntityFramework.Auth.Repositories.Queries; diff --git a/src/Infrastructure.EntityFramework/Auth/Repositories/WebAuthnCredentialRepository.cs b/src/Infrastructure.EntityFramework/Auth/Repositories/WebAuthnCredentialRepository.cs index b670a3f1de..e198a5f79d 100644 --- a/src/Infrastructure.EntityFramework/Auth/Repositories/WebAuthnCredentialRepository.cs +++ b/src/Infrastructure.EntityFramework/Auth/Repositories/WebAuthnCredentialRepository.cs @@ -1,7 +1,7 @@ using AutoMapper; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; -using Bit.Core.Auth.UserFeatures.UserKey; +using Bit.Core.KeyManagement.UserKey; using Bit.Infrastructure.EntityFramework.Auth.Models; using Bit.Infrastructure.EntityFramework.Repositories; using Microsoft.EntityFrameworkCore; diff --git a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs index 735625ce42..d234d25455 100644 --- a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs @@ -1,5 +1,5 @@ using AutoMapper; -using Bit.Core.Auth.UserFeatures.UserKey; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.Models; using Microsoft.EntityFrameworkCore; diff --git a/src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs b/src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs index 2db07f154b..adf3fcc1f1 100644 --- a/src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs +++ b/src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs @@ -1,7 +1,7 @@ #nullable enable using AutoMapper; -using Bit.Core.Auth.UserFeatures.UserKey; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Tools.Repositories; using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Repositories; diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index b94cbad7ce..c12167a78c 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -1,7 +1,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using AutoMapper; -using Bit.Core.Auth.UserFeatures.UserKey; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Utilities; using Bit.Core.Vault.Enums; using Bit.Core.Vault.Models.Data; diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/FolderRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/FolderRepository.cs index 0bab189de9..09ac256332 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/FolderRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/FolderRepository.cs @@ -1,5 +1,5 @@ using AutoMapper; -using Bit.Core.Auth.UserFeatures.UserKey; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Vault.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Vault.Models; diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index 13c80f8563..4a0a29a5d4 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -4,7 +4,7 @@ using Bit.Api.Auth.Controllers; using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Auth.Models.Request.WebAuthn; -using Bit.Api.Auth.Validators; +using Bit.Api.KeyManagement.Validators; using Bit.Api.Tools.Models.Request; using Bit.Api.Vault.Models.Request; using Bit.Core; @@ -14,12 +14,12 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; -using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; diff --git a/test/Api.Test/Vault/Validators/CipherRotationValidatorTests.cs b/test/Api.Test/KeyManagement/Validators/CipherRotationValidatorTests.cs similarity index 94% rename from test/Api.Test/Vault/Validators/CipherRotationValidatorTests.cs rename to test/Api.Test/KeyManagement/Validators/CipherRotationValidatorTests.cs index 632bb49676..a4633e78cb 100644 --- a/test/Api.Test/Vault/Validators/CipherRotationValidatorTests.cs +++ b/test/Api.Test/KeyManagement/Validators/CipherRotationValidatorTests.cs @@ -1,5 +1,5 @@ -using Bit.Api.Vault.Models.Request; -using Bit.Api.Vault.Validators; +using Bit.Api.KeyManagement.Validators; +using Bit.Api.Vault.Models.Request; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Vault.Models.Data; @@ -9,7 +9,7 @@ using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; -namespace Bit.Api.Test.Vault.Validators; +namespace Bit.Api.Test.KeyManagement.Validators; [SutProviderCustomize] public class CipherRotationValidatorTests diff --git a/test/Api.Test/Auth/Validators/EmergencyAccessRotationValidatorTests.cs b/test/Api.Test/KeyManagement/Validators/EmergencyAccessRotationValidatorTests.cs similarity index 98% rename from test/Api.Test/Auth/Validators/EmergencyAccessRotationValidatorTests.cs rename to test/Api.Test/KeyManagement/Validators/EmergencyAccessRotationValidatorTests.cs index c75ccd6437..e00129fd89 100644 --- a/test/Api.Test/Auth/Validators/EmergencyAccessRotationValidatorTests.cs +++ b/test/Api.Test/KeyManagement/Validators/EmergencyAccessRotationValidatorTests.cs @@ -1,5 +1,5 @@ using Bit.Api.Auth.Models.Request; -using Bit.Api.Auth.Validators; +using Bit.Api.KeyManagement.Validators; using Bit.Core.Auth.Models.Data; using Bit.Core.Entities; using Bit.Core.Exceptions; @@ -10,7 +10,7 @@ using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; -namespace Bit.Api.Test.Auth.Validators; +namespace Bit.Api.Test.KeyManagement.Validators; [SutProviderCustomize] public class EmergencyAccessRotationValidatorTests diff --git a/test/Api.Test/Vault/Validators/FolderRotationValidatorTests.cs b/test/Api.Test/KeyManagement/Validators/FolderRotationValidatorTests.cs similarity index 94% rename from test/Api.Test/Vault/Validators/FolderRotationValidatorTests.cs rename to test/Api.Test/KeyManagement/Validators/FolderRotationValidatorTests.cs index 0888fd32d4..3778741bbc 100644 --- a/test/Api.Test/Vault/Validators/FolderRotationValidatorTests.cs +++ b/test/Api.Test/KeyManagement/Validators/FolderRotationValidatorTests.cs @@ -1,5 +1,5 @@ -using Bit.Api.Vault.Models.Request; -using Bit.Api.Vault.Validators; +using Bit.Api.KeyManagement.Validators; +using Bit.Api.Vault.Models.Request; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Vault.Entities; @@ -9,7 +9,7 @@ using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; -namespace Bit.Api.Test.Vault.Validators; +namespace Bit.Api.Test.KeyManagement.Validators; [SutProviderCustomize] public class FolderRotationValidatorTests diff --git a/test/Api.Test/AdminConsole/Validators/OrganizationUserRotationValidatorTests.cs b/test/Api.Test/KeyManagement/Validators/OrganizationUserRotationValidatorTests.cs similarity index 98% rename from test/Api.Test/AdminConsole/Validators/OrganizationUserRotationValidatorTests.cs rename to test/Api.Test/KeyManagement/Validators/OrganizationUserRotationValidatorTests.cs index 5d4ffeef60..964c801903 100644 --- a/test/Api.Test/AdminConsole/Validators/OrganizationUserRotationValidatorTests.cs +++ b/test/Api.Test/KeyManagement/Validators/OrganizationUserRotationValidatorTests.cs @@ -1,5 +1,5 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; -using Bit.Api.AdminConsole.Validators; +using Bit.Api.KeyManagement.Validators; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Repositories; @@ -8,7 +8,7 @@ using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; -namespace Bit.Api.Test.AdminConsole.Validators; +namespace Bit.Api.Test.KeyManagement.Validators; [SutProviderCustomize] public class OrganizationUserRotationValidatorTests diff --git a/test/Api.Test/Tools/Validators/SendRotationValidatorTests.cs b/test/Api.Test/KeyManagement/Validators/SendRotationValidatorTests.cs similarity index 98% rename from test/Api.Test/Tools/Validators/SendRotationValidatorTests.cs rename to test/Api.Test/KeyManagement/Validators/SendRotationValidatorTests.cs index 76f938d1c4..842343ba33 100644 --- a/test/Api.Test/Tools/Validators/SendRotationValidatorTests.cs +++ b/test/Api.Test/KeyManagement/Validators/SendRotationValidatorTests.cs @@ -1,7 +1,7 @@ using System.Text.Json; +using Bit.Api.KeyManagement.Validators; using Bit.Api.Tools.Models; using Bit.Api.Tools.Models.Request; -using Bit.Api.Tools.Validators; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Tools.Entities; @@ -14,7 +14,7 @@ using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; -namespace Bit.Api.Test.Tools.Validators; +namespace Bit.Api.Test.KeyManagement.Validators; [SutProviderCustomize] public class SendRotationValidatorTests diff --git a/test/Api.Test/Auth/Validators/WebauthnLoginKeyRotationValidatorTests.cs b/test/Api.Test/KeyManagement/Validators/WebauthnLoginKeyRotationValidatorTests.cs similarity index 97% rename from test/Api.Test/Auth/Validators/WebauthnLoginKeyRotationValidatorTests.cs rename to test/Api.Test/KeyManagement/Validators/WebauthnLoginKeyRotationValidatorTests.cs index 97eadcbdc3..de661497e4 100644 --- a/test/Api.Test/Auth/Validators/WebauthnLoginKeyRotationValidatorTests.cs +++ b/test/Api.Test/KeyManagement/Validators/WebauthnLoginKeyRotationValidatorTests.cs @@ -1,5 +1,5 @@ using Bit.Api.Auth.Models.Request.WebAuthn; -using Bit.Api.Auth.Validators; +using Bit.Api.KeyManagement.Validators; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Repositories; using Bit.Core.Entities; @@ -9,7 +9,7 @@ using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; -namespace Bit.Api.Test.Auth.Validators; +namespace Bit.Api.Test.KeyManagement.Validators; [SutProviderCustomize] public class WebAuthnLoginKeyRotationValidatorTests diff --git a/test/Core.Test/Auth/UserFeatures/UserKey/RotateUserKeyCommandTests.cs b/test/Core.Test/KeyManagement/UserKey/RotateUserKeyCommandTests.cs similarity index 95% rename from test/Core.Test/Auth/UserFeatures/UserKey/RotateUserKeyCommandTests.cs rename to test/Core.Test/KeyManagement/UserKey/RotateUserKeyCommandTests.cs index 41c78f4272..b650d17240 100644 --- a/test/Core.Test/Auth/UserFeatures/UserKey/RotateUserKeyCommandTests.cs +++ b/test/Core.Test/KeyManagement/UserKey/RotateUserKeyCommandTests.cs @@ -1,8 +1,8 @@ using Bit.Core.Auth.Entities; -using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; -using Bit.Core.Auth.UserFeatures.UserKey.Implementations; 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; @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Identity; using NSubstitute; using Xunit; -namespace Bit.Core.Test.Auth.UserFeatures.UserKey; +namespace Bit.Core.Test.KeyManagement.UserFeatures.UserKey; [SutProviderCustomize] public class RotateUserKeyCommandTests From 718ff219ed7217825afbd6c86eb8593790b60d7b Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:09:41 -0600 Subject: [PATCH 07/94] [PM-13706] Add repository + stored procedures for private key regeneration (#4898) * Add stored procedure * Add repository --- .../Models/Data/UserAsymmetricKeys.cs | 9 +++++ .../IUserAsymmetricKeysRepository.cs | 9 +++++ .../DapperServiceCollectionExtensions.cs | 3 ++ .../UserAsymmetricKeysRepository.cs | 36 +++++++++++++++++++ ...ityFrameworkServiceCollectionExtensions.cs | 3 ++ .../UserAsymmetricKeysRepository.cs | 34 ++++++++++++++++++ .../UserAsymmetricKeys_Regenerate.sql | 16 +++++++++ ...-21_00_AddUserAsymmetricKeysRegenerate.sql | 16 +++++++++ 8 files changed, 126 insertions(+) create mode 100644 src/Core/KeyManagement/Models/Data/UserAsymmetricKeys.cs create mode 100644 src/Core/KeyManagement/Repositories/IUserAsymmetricKeysRepository.cs create mode 100644 src/Infrastructure.Dapper/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs create mode 100644 src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs create mode 100644 src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql create mode 100644 util/Migrator/DbScripts/2024-11-21_00_AddUserAsymmetricKeysRegenerate.sql diff --git a/src/Core/KeyManagement/Models/Data/UserAsymmetricKeys.cs b/src/Core/KeyManagement/Models/Data/UserAsymmetricKeys.cs new file mode 100644 index 0000000000..3c13629092 --- /dev/null +++ b/src/Core/KeyManagement/Models/Data/UserAsymmetricKeys.cs @@ -0,0 +1,9 @@ +#nullable enable +namespace Bit.Core.KeyManagement.Models.Data; + +public class UserAsymmetricKeys +{ + public Guid UserId { get; set; } + public required string PublicKey { get; set; } + public required string UserKeyEncryptedPrivateKey { get; set; } +} diff --git a/src/Core/KeyManagement/Repositories/IUserAsymmetricKeysRepository.cs b/src/Core/KeyManagement/Repositories/IUserAsymmetricKeysRepository.cs new file mode 100644 index 0000000000..fee9aee3bb --- /dev/null +++ b/src/Core/KeyManagement/Repositories/IUserAsymmetricKeysRepository.cs @@ -0,0 +1,9 @@ +#nullable enable +using Bit.Core.KeyManagement.Models.Data; + +namespace Bit.Core.KeyManagement.Repositories; + +public interface IUserAsymmetricKeysRepository +{ + Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys); +} diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index 550c572cf5..c873f84aa0 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Repositories; using Bit.Core.Billing.Repositories; +using Bit.Core.KeyManagement.Repositories; using Bit.Core.NotificationCenter.Repositories; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Repositories; @@ -9,6 +10,7 @@ using Bit.Core.Vault.Repositories; using Bit.Infrastructure.Dapper.AdminConsole.Repositories; using Bit.Infrastructure.Dapper.Auth.Repositories; using Bit.Infrastructure.Dapper.Billing.Repositories; +using Bit.Infrastructure.Dapper.KeyManagement.Repositories; using Bit.Infrastructure.Dapper.NotificationCenter.Repositories; using Bit.Infrastructure.Dapper.Repositories; using Bit.Infrastructure.Dapper.SecretsManager.Repositories; @@ -60,6 +62,7 @@ public static class DapperServiceCollectionExtensions .AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); if (selfHosted) { diff --git a/src/Infrastructure.Dapper/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs b/src/Infrastructure.Dapper/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs new file mode 100644 index 0000000000..f176327f4f --- /dev/null +++ b/src/Infrastructure.Dapper/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs @@ -0,0 +1,36 @@ +#nullable enable +using System.Data; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Repositories; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; + +namespace Bit.Infrastructure.Dapper.KeyManagement.Repositories; + +public class UserAsymmetricKeysRepository : BaseRepository, IUserAsymmetricKeysRepository +{ + public UserAsymmetricKeysRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { + } + + public UserAsymmetricKeysRepository(string connectionString, string readOnlyConnectionString) : base( + connectionString, readOnlyConnectionString) + { + } + + public async Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys) + { + await using var connection = new SqlConnection(ConnectionString); + + await connection.ExecuteAsync("[dbo].[UserAsymmetricKeys_Regenerate]", + new + { + userAsymmetricKeys.UserId, + userAsymmetricKeys.PublicKey, + PrivateKey = userAsymmetricKeys.UserKeyEncryptedPrivateKey + }, commandType: CommandType.StoredProcedure); + } +} diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index b8c84f649f..b2eefe4523 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Billing.Repositories; using Bit.Core.Enums; +using Bit.Core.KeyManagement.Repositories; using Bit.Core.NotificationCenter.Repositories; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Repositories; @@ -10,6 +11,7 @@ using Bit.Core.Vault.Repositories; using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; using Bit.Infrastructure.EntityFramework.Auth.Repositories; using Bit.Infrastructure.EntityFramework.Billing.Repositories; +using Bit.Infrastructure.EntityFramework.KeyManagement.Repositories; using Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.SecretsManager.Repositories; @@ -97,6 +99,7 @@ public static class EntityFrameworkServiceCollectionExtensions .AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); if (selfHosted) { diff --git a/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs b/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs new file mode 100644 index 0000000000..c680424f56 --- /dev/null +++ b/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs @@ -0,0 +1,34 @@ +#nullable enable +using AutoMapper; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Infrastructure.EntityFramework.KeyManagement.Repositories; + +public class UserAsymmetricKeysRepository : BaseEntityFrameworkRepository, IUserAsymmetricKeysRepository +{ + public UserAsymmetricKeysRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : base( + serviceScopeFactory, + mapper) + { + } + + public async Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys) + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + + var entity = await dbContext.Users.FindAsync(userAsymmetricKeys.UserId); + if (entity != null) + { + var utcNow = DateTime.UtcNow; + entity.PublicKey = userAsymmetricKeys.PublicKey; + entity.PrivateKey = userAsymmetricKeys.UserKeyEncryptedPrivateKey; + entity.RevisionDate = utcNow; + entity.AccountRevisionDate = utcNow; + await dbContext.SaveChangesAsync(); + } + } +} diff --git a/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql b/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql new file mode 100644 index 0000000000..26d0c40183 --- /dev/null +++ b/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql @@ -0,0 +1,16 @@ +CREATE PROCEDURE [dbo].[UserAsymmetricKeys_Regenerate] + @UserId UNIQUEIDENTIFIER, + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX) +AS +BEGIN + SET NOCOUNT ON + DECLARE @UtcNow DATETIME2(7) = GETUTCDATE(); + + UPDATE [dbo].[User] + SET [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey, + [RevisionDate] = @UtcNow, + [AccountRevisionDate] = @UtcNow + WHERE [Id] = @UserId +END diff --git a/util/Migrator/DbScripts/2024-11-21_00_AddUserAsymmetricKeysRegenerate.sql b/util/Migrator/DbScripts/2024-11-21_00_AddUserAsymmetricKeysRegenerate.sql new file mode 100644 index 0000000000..e1f5431145 --- /dev/null +++ b/util/Migrator/DbScripts/2024-11-21_00_AddUserAsymmetricKeysRegenerate.sql @@ -0,0 +1,16 @@ +CREATE OR ALTER PROCEDURE [dbo].[UserAsymmetricKeys_Regenerate] + @UserId UNIQUEIDENTIFIER, + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX) +AS +BEGIN + SET NOCOUNT ON + DECLARE @UtcNow DATETIME2(7) = GETUTCDATE(); + + UPDATE [dbo].[User] + SET [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey, + [RevisionDate] = @UtcNow, + [AccountRevisionDate] = @UtcNow + WHERE [Id] = @UserId +END From 5dbda8c831ea9562ff04724bde24b4ec39662626 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 22 Nov 2024 12:21:47 +0100 Subject: [PATCH 08/94] [deps] Tools: Update aws-sdk-net monorepo (#5056) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 913c4b3877..b8e2b8409a 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,8 +21,8 @@ - - + + From f4dd794cba71fd9d99d451a48d8b14a075731529 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:01:45 +0000 Subject: [PATCH 09/94] [deps] Platform: Update Quartz to 3.13.1 (#4655) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index b8e2b8409a..2f40fb7b23 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -44,7 +44,7 @@ - + From dac8f66a598d2369af81e6d55dc79489e58d3365 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:05:15 -0500 Subject: [PATCH 10/94] Resolve AC Warnings (#4644) * Resolve AC Warnings * Remove Unneeded Changes * Add Back RequiredAttribute * Format --- src/Admin/AdminConsole/Models/OrganizationEditModel.cs | 2 +- src/Api/AdminConsole/Public/Models/MemberBaseModel.cs | 9 ++++++--- .../Public/Models/Response/MemberResponseModel.cs | 3 +++ .../Models/Mail/Provider/ProviderInitiateDeleteModel.cs | 2 -- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs index 4ba22130f7..48340df708 100644 --- a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs @@ -143,7 +143,7 @@ public class OrganizationEditModel : OrganizationViewModel [Display(Name = "SCIM")] public bool UseScim { get; set; } [Display(Name = "Secrets Manager")] - public bool UseSecretsManager { get; set; } + public new bool UseSecretsManager { get; set; } [Display(Name = "Self Host")] public bool SelfHost { get; set; } [Display(Name = "Users Get Premium")] diff --git a/src/Api/AdminConsole/Public/Models/MemberBaseModel.cs b/src/Api/AdminConsole/Public/Models/MemberBaseModel.cs index c56117ae71..dc3f91d49f 100644 --- a/src/Api/AdminConsole/Public/Models/MemberBaseModel.cs +++ b/src/Api/AdminConsole/Public/Models/MemberBaseModel.cs @@ -1,8 +1,11 @@ using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationUsers; +#nullable enable + namespace Bit.Api.AdminConsole.Public.Models; public abstract class MemberBaseModel @@ -25,6 +28,7 @@ public abstract class MemberBaseModel } } + [SetsRequiredMembers] public MemberBaseModel(OrganizationUserUserDetails user) { if (user == null) @@ -46,14 +50,13 @@ public abstract class MemberBaseModel /// [Required] [EnumDataType(typeof(OrganizationUserType))] - public OrganizationUserType? Type { get; set; } + public required OrganizationUserType? Type { get; set; } /// /// External identifier for reference or linking this member to another system, such as a user directory. /// /// external_id_123456 [StringLength(300)] - public string ExternalId { get; set; } - + public string? ExternalId { get; set; } /// /// The member's custom permissions if the member has a Custom role. If not supplied, all custom permissions will /// default to false. diff --git a/src/Api/AdminConsole/Public/Models/Response/MemberResponseModel.cs b/src/Api/AdminConsole/Public/Models/Response/MemberResponseModel.cs index ab6ecbca44..499c27cfc9 100644 --- a/src/Api/AdminConsole/Public/Models/Response/MemberResponseModel.cs +++ b/src/Api/AdminConsole/Public/Models/Response/MemberResponseModel.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using Bit.Api.Models.Public.Response; using Bit.Core.Entities; @@ -16,6 +17,7 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel [JsonConstructor] public MemberResponseModel() { } + [SetsRequiredMembers] public MemberResponseModel(OrganizationUser user, IEnumerable collections) : base(user) { if (user == null) @@ -31,6 +33,7 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel ResetPasswordEnrolled = user.ResetPasswordKey != null; } + [SetsRequiredMembers] public MemberResponseModel(OrganizationUserUserDetails user, bool twoFactorEnabled, IEnumerable collections) : base(user) { diff --git a/src/Core/Models/Mail/Provider/ProviderInitiateDeleteModel.cs b/src/Core/Models/Mail/Provider/ProviderInitiateDeleteModel.cs index 196decb5ee..a5071527fe 100644 --- a/src/Core/Models/Mail/Provider/ProviderInitiateDeleteModel.cs +++ b/src/Core/Models/Mail/Provider/ProviderInitiateDeleteModel.cs @@ -8,10 +8,8 @@ public class ProviderInitiateDeleteModel : BaseMailModel Token, ProviderNameUrlEncoded); - public string WebVaultUrl { get; set; } public string Token { get; set; } public Guid ProviderId { get; set; } - public string SiteName { get; set; } public string ProviderName { get; set; } public string ProviderNameUrlEncoded { get; set; } public string ProviderBillingEmail { get; set; } From c4ab5f31f5979319d4b7a9d3189117edaca3304c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:12:04 +0100 Subject: [PATCH 11/94] [deps] Tools: Update aws-sdk-net monorepo (#5065) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 2f40fb7b23..d16624898d 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,8 +21,8 @@ - - + + From 07592e22b9f4102e118d817c6384a7c4da604195 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:17:59 +0100 Subject: [PATCH 12/94] [deps]: Update Microsoft.NET.Test.Sdk to 17.12.0 (#5067) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- .../Infrastructure.Dapper.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj b/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj index dfc8951cc3..82d63bd3c1 100644 --- a/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj +++ b/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj @@ -10,7 +10,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive From fd7ff2ac63f40cbee8457af1aa44e6bc9a1c0070 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:30:02 -0500 Subject: [PATCH 13/94] [deps] Billing: Update FluentAssertions to 6.12.2 (#5015) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> --- test/Billing.Test/Billing.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Billing.Test/Billing.Test.csproj b/test/Billing.Test/Billing.Test.csproj index 3bbda52ded..c6a7ef48e0 100644 --- a/test/Billing.Test/Billing.Test.csproj +++ b/test/Billing.Test/Billing.Test.csproj @@ -6,7 +6,7 @@ - + From b974899127ee0154b20fb8ad8d254b17194ee523 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:30:32 -0500 Subject: [PATCH 14/94] [deps] Billing: Update Braintree to 5.28.0 (#5019) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> --- src/Core/Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index d16624898d..5970629caa 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -54,7 +54,7 @@ - + From 8f703a29ac4dd46d8f5eacee09b9fbb73ede9031 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:20:42 -0500 Subject: [PATCH 15/94] [deps] DbOps: Update Microsoft.Azure.Cosmos to 3.46.0 (#5066) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 5970629caa..fd4d8cc7e1 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -36,7 +36,7 @@ - + From 1b75e35c319f588d9e8a5fe7fd9ccd6591813cf1 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Tue, 26 Nov 2024 16:37:12 -0600 Subject: [PATCH 16/94] [PM-10319] - Revoke Non Complaint Users for 2FA and Single Org Policy Enablement (#5037) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Revoking users when enabling single org and 2fa policies. - Updated emails sent when users are revoked via 2FA or Single Organization policy enablement Co-authored-by: Matt Bishop Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> --- .../AdminConsole/Enums/EventSystemUser.cs | 1 + .../AdminConsole/Models/Data/IActingUser.cs | 10 + .../AdminConsole/Models/Data/StandardUser.cs | 16 ++ .../AdminConsole/Models/Data/SystemUser.cs | 16 ++ .../VerifyOrganizationDomainCommand.cs | 93 +++++---- ...vokeNonCompliantOrganizationUserCommand.cs | 9 + .../Requests/RevokeOrganizationUserRequest.cs | 13 ++ ...vokeNonCompliantOrganizationUserCommand.cs | 112 +++++++++++ .../Policies/Models/PolicyUpdate.cs | 2 + .../SingleOrgPolicyValidator.cs | 70 ++++++- .../TwoFactorAuthenticationPolicyValidator.cs | 69 ++++++- .../IOrganizationUserRepository.cs | 2 + .../AdminConsole/Services/IPolicyService.cs | 2 +- .../Services/Implementations/PolicyService.cs | 15 +- ...tionUserRevokedForSingleOrgPolicy.html.hbs | 14 ++ ...tionUserRevokedForSingleOrgPolicy.text.hbs | 5 + ...tionUserRevokedForTwoFactorPolicy.html.hbs | 15 ++ ...tionUserRevokedForTwoFactorPolicy.text.hbs | 7 + src/Core/Models/Commands/CommandResult.cs | 12 ++ ...nUserRevokedForPolicySingleOrgViewModel.cs | 6 + ...nUserRevokedForPolicyTwoFactorViewModel.cs | 6 + ...OrganizationServiceCollectionExtensions.cs | 1 + src/Core/Services/IMailService.cs | 2 + .../Implementations/HandlebarsMailService.cs | 31 ++- .../NoopImplementations/NoopMailService.cs | 6 + .../OrganizationUserRepository.cs | 10 + .../OrganizationUserRepository.cs | 12 ++ ...OrganizationUser_SetStatusForUsersById.sql | 29 +++ .../AutoFixture/PolicyUpdateFixtures.cs | 4 +- .../VerifyOrganizationDomainCommandTests.cs | 38 +++- ...onCompliantOrganizationUserCommandTests.cs | 185 ++++++++++++++++++ .../SingleOrgPolicyValidatorTests.cs | 151 ++++++++++++++ ...actorAuthenticationPolicyValidatorTests.cs | 150 +++++++++++++- .../Policies/SavePolicyCommandTests.cs | 4 +- .../Services/EventServiceTests.cs | 1 - .../2024-11-26-00_OrgUserSetStatusBulk.sql | 28 +++ 36 files changed, 1074 insertions(+), 73 deletions(-) create mode 100644 src/Core/AdminConsole/Models/Data/IActingUser.cs create mode 100644 src/Core/AdminConsole/Models/Data/StandardUser.cs create mode 100644 src/Core/AdminConsole/Models/Data/SystemUser.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeNonCompliantOrganizationUserCommand.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Requests/RevokeOrganizationUserRequest.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForSingleOrgPolicy.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForSingleOrgPolicy.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForTwoFactorPolicy.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForTwoFactorPolicy.text.hbs create mode 100644 src/Core/Models/Commands/CommandResult.cs create mode 100644 src/Core/Models/Mail/OrganizationUserRevokedForPolicySingleOrgViewModel.cs create mode 100644 src/Core/Models/Mail/OrganizationUserRevokedForPolicyTwoFactorViewModel.cs create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationUser_SetStatusForUsersById.sql create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommandTests.cs create mode 100644 util/Migrator/DbScripts/2024-11-26-00_OrgUserSetStatusBulk.sql diff --git a/src/Core/AdminConsole/Enums/EventSystemUser.cs b/src/Core/AdminConsole/Enums/EventSystemUser.cs index df9be9a350..c3e13705dd 100644 --- a/src/Core/AdminConsole/Enums/EventSystemUser.cs +++ b/src/Core/AdminConsole/Enums/EventSystemUser.cs @@ -2,6 +2,7 @@ public enum EventSystemUser : byte { + Unknown = 0, SCIM = 1, DomainVerification = 2, PublicApi = 3, diff --git a/src/Core/AdminConsole/Models/Data/IActingUser.cs b/src/Core/AdminConsole/Models/Data/IActingUser.cs new file mode 100644 index 0000000000..f97235f34c --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/IActingUser.cs @@ -0,0 +1,10 @@ +using Bit.Core.Enums; + +namespace Bit.Core.AdminConsole.Models.Data; + +public interface IActingUser +{ + Guid? UserId { get; } + bool IsOrganizationOwnerOrProvider { get; } + EventSystemUser? SystemUserType { get; } +} diff --git a/src/Core/AdminConsole/Models/Data/StandardUser.cs b/src/Core/AdminConsole/Models/Data/StandardUser.cs new file mode 100644 index 0000000000..f21a41db7c --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/StandardUser.cs @@ -0,0 +1,16 @@ +using Bit.Core.Enums; + +namespace Bit.Core.AdminConsole.Models.Data; + +public class StandardUser : IActingUser +{ + public StandardUser(Guid userId, bool isOrganizationOwner) + { + UserId = userId; + IsOrganizationOwnerOrProvider = isOrganizationOwner; + } + + public Guid? UserId { get; } + public bool IsOrganizationOwnerOrProvider { get; } + public EventSystemUser? SystemUserType => throw new Exception($"{nameof(StandardUser)} does not have a {nameof(SystemUserType)}"); +} diff --git a/src/Core/AdminConsole/Models/Data/SystemUser.cs b/src/Core/AdminConsole/Models/Data/SystemUser.cs new file mode 100644 index 0000000000..c4859f928f --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/SystemUser.cs @@ -0,0 +1,16 @@ +using Bit.Core.Enums; + +namespace Bit.Core.AdminConsole.Models.Data; + +public class SystemUser : IActingUser +{ + public SystemUser(EventSystemUser systemUser) + { + SystemUserType = systemUser; + } + + public Guid? UserId => throw new Exception($"{nameof(SystemUserType)} does not have a {nameof(UserId)}."); + + public bool IsOrganizationOwnerOrProvider => false; + public EventSystemUser? SystemUserType { get; } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs index 870fa72aa7..dd6add669f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs @@ -1,7 +1,9 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.Services; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -12,124 +14,121 @@ using Microsoft.Extensions.Logging; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; -public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand +public class VerifyOrganizationDomainCommand( + IOrganizationDomainRepository organizationDomainRepository, + IDnsResolverService dnsResolverService, + IEventService eventService, + IGlobalSettings globalSettings, + IPolicyService policyService, + IFeatureService featureService, + ICurrentContext currentContext, + ILogger logger) + : IVerifyOrganizationDomainCommand { - private readonly IOrganizationDomainRepository _organizationDomainRepository; - private readonly IDnsResolverService _dnsResolverService; - private readonly IEventService _eventService; - private readonly IGlobalSettings _globalSettings; - private readonly IPolicyService _policyService; - private readonly IFeatureService _featureService; - private readonly ILogger _logger; - - public VerifyOrganizationDomainCommand( - IOrganizationDomainRepository organizationDomainRepository, - IDnsResolverService dnsResolverService, - IEventService eventService, - IGlobalSettings globalSettings, - IPolicyService policyService, - IFeatureService featureService, - ILogger logger) - { - _organizationDomainRepository = organizationDomainRepository; - _dnsResolverService = dnsResolverService; - _eventService = eventService; - _globalSettings = globalSettings; - _policyService = policyService; - _featureService = featureService; - _logger = logger; - } public async Task UserVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain) { - var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain); + if (currentContext.UserId is null) + { + throw new InvalidOperationException( + $"{nameof(UserVerifyOrganizationDomainAsync)} can only be called by a user. " + + $"Please call {nameof(SystemVerifyOrganizationDomainAsync)} for system users."); + } - await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult, + var actingUser = new StandardUser(currentContext.UserId.Value, await currentContext.OrganizationOwner(organizationDomain.OrganizationId)); + + var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain, actingUser); + + await eventService.LogOrganizationDomainEventAsync(domainVerificationResult, domainVerificationResult.VerifiedDate != null ? EventType.OrganizationDomain_Verified : EventType.OrganizationDomain_NotVerified); - await _organizationDomainRepository.ReplaceAsync(domainVerificationResult); + await organizationDomainRepository.ReplaceAsync(domainVerificationResult); return domainVerificationResult; } public async Task SystemVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain) { + var actingUser = new SystemUser(EventSystemUser.DomainVerification); + organizationDomain.SetJobRunCount(); - var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain); + var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain, actingUser); if (domainVerificationResult.VerifiedDate is not null) { - _logger.LogInformation(Constants.BypassFiltersEventId, "Successfully validated domain"); + logger.LogInformation(Constants.BypassFiltersEventId, "Successfully validated domain"); - await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult, + await eventService.LogOrganizationDomainEventAsync(domainVerificationResult, EventType.OrganizationDomain_Verified, EventSystemUser.DomainVerification); } else { - domainVerificationResult.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval); + domainVerificationResult.SetNextRunDate(globalSettings.DomainVerification.VerificationInterval); - await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult, + await eventService.LogOrganizationDomainEventAsync(domainVerificationResult, EventType.OrganizationDomain_NotVerified, EventSystemUser.DomainVerification); - _logger.LogInformation(Constants.BypassFiltersEventId, + logger.LogInformation(Constants.BypassFiltersEventId, "Verification for organization {OrgId} with domain {Domain} failed", domainVerificationResult.OrganizationId, domainVerificationResult.DomainName); } - await _organizationDomainRepository.ReplaceAsync(domainVerificationResult); + await organizationDomainRepository.ReplaceAsync(domainVerificationResult); return domainVerificationResult; } - private async Task VerifyOrganizationDomainAsync(OrganizationDomain domain) + private async Task VerifyOrganizationDomainAsync(OrganizationDomain domain, IActingUser actingUser) { domain.SetLastCheckedDate(); if (domain.VerifiedDate is not null) { - await _organizationDomainRepository.ReplaceAsync(domain); + await organizationDomainRepository.ReplaceAsync(domain); throw new ConflictException("Domain has already been verified."); } var claimedDomain = - await _organizationDomainRepository.GetClaimedDomainsByDomainNameAsync(domain.DomainName); + await organizationDomainRepository.GetClaimedDomainsByDomainNameAsync(domain.DomainName); if (claimedDomain.Count > 0) { - await _organizationDomainRepository.ReplaceAsync(domain); + await organizationDomainRepository.ReplaceAsync(domain); throw new ConflictException("The domain is not available to be claimed."); } try { - if (await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt)) + if (await dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt)) { domain.SetVerifiedDate(); - await EnableSingleOrganizationPolicyAsync(domain.OrganizationId); + await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser); } } catch (Exception e) { - _logger.LogError("Error verifying Organization domain: {domain}. {errorMessage}", + logger.LogError("Error verifying Organization domain: {domain}. {errorMessage}", domain.DomainName, e.Message); } return domain; } - private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId) + private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser) { - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) + if (featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) { - await _policyService.SaveAsync( - new Policy { OrganizationId = organizationId, Type = PolicyType.SingleOrg, Enabled = true }, null); + await policyService.SaveAsync( + new Policy { OrganizationId = organizationId, Type = PolicyType.SingleOrg, Enabled = true }, + savingUserId: actingUser is StandardUser standardUser ? standardUser.UserId : null, + eventSystemUser: actingUser is SystemUser systemUser ? systemUser.SystemUserType : null); } } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeNonCompliantOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeNonCompliantOrganizationUserCommand.cs new file mode 100644 index 0000000000..c9768a8905 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeNonCompliantOrganizationUserCommand.cs @@ -0,0 +1,9 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; +using Bit.Core.Models.Commands; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; + +public interface IRevokeNonCompliantOrganizationUserCommand +{ + Task RevokeNonCompliantOrganizationUsersAsync(RevokeOrganizationUsersRequest request); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Requests/RevokeOrganizationUserRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Requests/RevokeOrganizationUserRequest.cs new file mode 100644 index 0000000000..88f1dc8aa1 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Requests/RevokeOrganizationUserRequest.cs @@ -0,0 +1,13 @@ +using Bit.Core.AdminConsole.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; + +public record RevokeOrganizationUsersRequest( + Guid OrganizationId, + IEnumerable OrganizationUsers, + IActingUser ActionPerformedBy) +{ + public RevokeOrganizationUsersRequest(Guid organizationId, OrganizationUserUserDetails organizationUser, IActingUser actionPerformedBy) + : this(organizationId, [organizationUser], actionPerformedBy) { } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs new file mode 100644 index 0000000000..971ed02b29 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs @@ -0,0 +1,112 @@ +using Bit.Core.AdminConsole.Models.Data; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; +using Bit.Core.Enums; +using Bit.Core.Models.Commands; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using Bit.Core.Services; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; + +public class RevokeNonCompliantOrganizationUserCommand(IOrganizationUserRepository organizationUserRepository, + IEventService eventService, + IHasConfirmedOwnersExceptQuery confirmedOwnersExceptQuery, + TimeProvider timeProvider) : IRevokeNonCompliantOrganizationUserCommand +{ + public const string ErrorCannotRevokeSelf = "You cannot revoke yourself."; + public const string ErrorOnlyOwnersCanRevokeOtherOwners = "Only owners can revoke other owners."; + public const string ErrorUserAlreadyRevoked = "User is already revoked."; + public const string ErrorOrgMustHaveAtLeastOneOwner = "Organization must have at least one confirmed owner."; + public const string ErrorInvalidUsers = "Invalid users."; + public const string ErrorRequestedByWasNotValid = "Action was performed by an unexpected type."; + + public async Task RevokeNonCompliantOrganizationUsersAsync(RevokeOrganizationUsersRequest request) + { + var validationResult = await ValidateAsync(request); + + if (validationResult.HasErrors) + { + return validationResult; + } + + await organizationUserRepository.RevokeManyByIdAsync(request.OrganizationUsers.Select(x => x.Id)); + + var now = timeProvider.GetUtcNow(); + + switch (request.ActionPerformedBy) + { + case StandardUser: + await eventService.LogOrganizationUserEventsAsync( + request.OrganizationUsers.Select(x => GetRevokedUserEventTuple(x, now))); + break; + case SystemUser { SystemUserType: not null } loggableSystem: + await eventService.LogOrganizationUserEventsAsync( + request.OrganizationUsers.Select(x => + GetRevokedUserEventBySystemUserTuple(x, loggableSystem.SystemUserType.Value, now))); + break; + } + + return validationResult; + } + + private static (OrganizationUserUserDetails organizationUser, EventType eventType, DateTime? time) GetRevokedUserEventTuple( + OrganizationUserUserDetails organizationUser, DateTimeOffset dateTimeOffset) => + new(organizationUser, EventType.OrganizationUser_Revoked, dateTimeOffset.UtcDateTime); + + private static (OrganizationUserUserDetails organizationUser, EventType eventType, EventSystemUser eventSystemUser, DateTime? time) GetRevokedUserEventBySystemUserTuple( + OrganizationUserUserDetails organizationUser, EventSystemUser systemUser, DateTimeOffset dateTimeOffset) => new(organizationUser, + EventType.OrganizationUser_Revoked, systemUser, dateTimeOffset.UtcDateTime); + + private async Task ValidateAsync(RevokeOrganizationUsersRequest request) + { + if (!PerformedByIsAnExpectedType(request.ActionPerformedBy)) + { + return new CommandResult(ErrorRequestedByWasNotValid); + } + + if (request.ActionPerformedBy is StandardUser user + && request.OrganizationUsers.Any(x => x.UserId == user.UserId)) + { + return new CommandResult(ErrorCannotRevokeSelf); + } + + if (request.OrganizationUsers.Any(x => x.OrganizationId != request.OrganizationId)) + { + return new CommandResult(ErrorInvalidUsers); + } + + if (!await confirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync( + request.OrganizationId, + request.OrganizationUsers.Select(x => x.Id))) + { + return new CommandResult(ErrorOrgMustHaveAtLeastOneOwner); + } + + return request.OrganizationUsers.Aggregate(new CommandResult(), (result, userToRevoke) => + { + if (IsAlreadyRevoked(userToRevoke)) + { + result.ErrorMessages.Add($"{ErrorUserAlreadyRevoked} Id: {userToRevoke.Id}"); + return result; + } + + if (NonOwnersCannotRevokeOwners(userToRevoke, request.ActionPerformedBy)) + { + result.ErrorMessages.Add($"{ErrorOnlyOwnersCanRevokeOtherOwners}"); + return result; + } + + return result; + }); + } + + private static bool PerformedByIsAnExpectedType(IActingUser entity) => entity is SystemUser or StandardUser; + + private static bool IsAlreadyRevoked(OrganizationUserUserDetails organizationUser) => + organizationUser is { Status: OrganizationUserStatusType.Revoked }; + + private static bool NonOwnersCannotRevokeOwners(OrganizationUserUserDetails organizationUser, + IActingUser actingUser) => + actingUser is StandardUser { IsOrganizationOwnerOrProvider: false } && organizationUser.Type == OrganizationUserType.Owner; +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/PolicyUpdate.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/PolicyUpdate.cs index 117a7ec733..d1a52f0080 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/PolicyUpdate.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/PolicyUpdate.cs @@ -1,6 +1,7 @@ #nullable enable using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.Utilities; @@ -15,6 +16,7 @@ public record PolicyUpdate public PolicyType Type { get; set; } public string? Data { get; set; } public bool Enabled { get; set; } + public IActingUser? PerformedBy { get; set; } public T GetDataModel() where T : IPolicyDataModel, new() { diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs index cc6971f946..050949ee7f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs @@ -2,8 +2,10 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; @@ -18,6 +20,8 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; public class SingleOrgPolicyValidator : IPolicyValidator { public PolicyType Type => PolicyType.SingleOrg; + private const string OrganizationNotFoundErrorMessage = "Organization not found."; + private const string ClaimedDomainSingleOrganizationRequiredErrorMessage = "The Single organization policy is required for organizations that have enabled domain verification."; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IMailService _mailService; @@ -27,6 +31,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator private readonly IFeatureService _featureService; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; + private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand; public SingleOrgPolicyValidator( IOrganizationUserRepository organizationUserRepository, @@ -36,7 +41,8 @@ public class SingleOrgPolicyValidator : IPolicyValidator ICurrentContext currentContext, IFeatureService featureService, IRemoveOrganizationUserCommand removeOrganizationUserCommand, - IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery) + IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery, + IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand) { _organizationUserRepository = organizationUserRepository; _mailService = mailService; @@ -46,6 +52,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator _featureService = featureService; _removeOrganizationUserCommand = removeOrganizationUserCommand; _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; + _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand; } public IEnumerable RequiredPolicies => []; @@ -54,10 +61,54 @@ public class SingleOrgPolicyValidator : IPolicyValidator { if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true }) { - await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId); + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) + { + var currentUser = _currentContext.UserId ?? Guid.Empty; + var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId); + await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider)); + } + else + { + await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId); + } } } + private async Task RevokeNonCompliantUsersAsync(Guid organizationId, IActingUser performedBy) + { + var organization = await _organizationRepository.GetByIdAsync(organizationId); + + if (organization is null) + { + throw new NotFoundException(OrganizationNotFoundErrorMessage); + } + + var currentActiveRevocableOrganizationUsers = + (await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId)) + .Where(ou => ou.Status != OrganizationUserStatusType.Invited && + ou.Status != OrganizationUserStatusType.Revoked && + ou.Type != OrganizationUserType.Owner && + ou.Type != OrganizationUserType.Admin && + !(performedBy is StandardUser stdUser && stdUser.UserId == ou.UserId)) + .ToList(); + + if (currentActiveRevocableOrganizationUsers.Count == 0) + { + return; + } + + var commandResult = await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync( + new RevokeOrganizationUsersRequest(organizationId, currentActiveRevocableOrganizationUsers, performedBy)); + + if (commandResult.HasErrors) + { + throw new BadRequestException(string.Join(", ", commandResult.ErrorMessages)); + } + + await Task.WhenAll(currentActiveRevocableOrganizationUsers.Select(x => + _mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email))); + } + private async Task RemoveNonCompliantUsersAsync(Guid organizationId) { // Remove non-compliant users @@ -67,7 +118,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator var org = await _organizationRepository.GetByIdAsync(organizationId); if (org == null) { - throw new NotFoundException("Organization not found."); + throw new NotFoundException(OrganizationNotFoundErrorMessage); } var removableOrgUsers = orgUsers.Where(ou => @@ -76,18 +127,17 @@ public class SingleOrgPolicyValidator : IPolicyValidator ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin && ou.UserId != savingUserId - ).ToList(); + ).ToList(); var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync( - removableOrgUsers.Select(ou => ou.UserId!.Value)); + removableOrgUsers.Select(ou => ou.UserId!.Value)); foreach (var orgUser in removableOrgUsers) { if (userOrgs.Any(ou => ou.UserId == orgUser.UserId - && ou.OrganizationId != org.Id - && ou.Status != OrganizationUserStatusType.Invited)) + && ou.OrganizationId != org.Id + && ou.Status != OrganizationUserStatusType.Invited)) { - await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id, - savingUserId); + await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id, savingUserId); await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync( org.DisplayName(), orgUser.Email); @@ -111,7 +161,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)) { - return "The Single organization policy is required for organizations that have enabled domain verification."; + return ClaimedDomainSingleOrganizationRequiredErrorMessage; } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs index ef896bbb9b..c2dd8cff91 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs @@ -2,12 +2,15 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; @@ -21,6 +24,10 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator private readonly ICurrentContext _currentContext; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; + private readonly IFeatureService _featureService; + private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand; + + public const string NonCompliantMembersWillLoseAccessMessage = "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page."; public PolicyType Type => PolicyType.TwoFactorAuthentication; public IEnumerable RequiredPolicies => []; @@ -31,7 +38,9 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator IOrganizationRepository organizationRepository, ICurrentContext currentContext, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IRemoveOrganizationUserCommand removeOrganizationUserCommand) + IRemoveOrganizationUserCommand removeOrganizationUserCommand, + IFeatureService featureService, + IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand) { _organizationUserRepository = organizationUserRepository; _mailService = mailService; @@ -39,16 +48,65 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator _currentContext = currentContext; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _removeOrganizationUserCommand = removeOrganizationUserCommand; + _featureService = featureService; + _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand; } public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) { if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true }) { - await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId); + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) + { + var currentUser = _currentContext.UserId ?? Guid.Empty; + var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId); + await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider)); + } + else + { + await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId); + } } } + private async Task RevokeNonCompliantUsersAsync(Guid organizationId, IActingUser performedBy) + { + var organization = await _organizationRepository.GetByIdAsync(organizationId); + + var currentActiveRevocableOrganizationUsers = + (await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId)) + .Where(ou => ou.Status != OrganizationUserStatusType.Invited && + ou.Status != OrganizationUserStatusType.Revoked && + ou.Type != OrganizationUserType.Owner && + ou.Type != OrganizationUserType.Admin && + !(performedBy is StandardUser stdUser && stdUser.UserId == ou.UserId)) + .ToList(); + + if (currentActiveRevocableOrganizationUsers.Count == 0) + { + return; + } + + var organizationUsersTwoFactorEnabled = + await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(currentActiveRevocableOrganizationUsers); + + if (NonCompliantMembersWillLoseAccess(currentActiveRevocableOrganizationUsers, organizationUsersTwoFactorEnabled)) + { + throw new BadRequestException(NonCompliantMembersWillLoseAccessMessage); + } + + var commandResult = await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync( + new RevokeOrganizationUsersRequest(organizationId, currentActiveRevocableOrganizationUsers, performedBy)); + + if (commandResult.HasErrors) + { + throw new BadRequestException(string.Join(", ", commandResult.ErrorMessages)); + } + + await Task.WhenAll(currentActiveRevocableOrganizationUsers.Select(x => + _mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email))); + } + private async Task RemoveNonCompliantUsersAsync(Guid organizationId) { var org = await _organizationRepository.GetByIdAsync(organizationId); @@ -83,5 +141,12 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator } } + private static bool NonCompliantMembersWillLoseAccess( + IEnumerable orgUserDetails, + IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) => + orgUserDetails.Any(x => + !x.HasMasterPassword && !organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == x.Id) + .isTwoFactorEnabled); + public Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(""); } diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs index cb540c212b..516b4614af 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs @@ -58,4 +58,6 @@ public interface IOrganizationUserRepository : IRepository Task> GetManyByOrganizationWithClaimedDomainsAsync(Guid organizationId); + + Task RevokeManyByIdAsync(IEnumerable organizationUserIds); } diff --git a/src/Core/AdminConsole/Services/IPolicyService.cs b/src/Core/AdminConsole/Services/IPolicyService.cs index 16ff2f4fa1..715c3a34d9 100644 --- a/src/Core/AdminConsole/Services/IPolicyService.cs +++ b/src/Core/AdminConsole/Services/IPolicyService.cs @@ -9,7 +9,7 @@ namespace Bit.Core.AdminConsole.Services; public interface IPolicyService { - Task SaveAsync(Policy policy, Guid? savingUserId); + Task SaveAsync(Policy policy, Guid? savingUserId, EventSystemUser? eventSystemUser = null); /// /// Get the combined master password policy options for the specified user. diff --git a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs index 42655040a3..69ec27fd8a 100644 --- a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs +++ b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; @@ -9,6 +10,7 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -34,6 +36,7 @@ public class PolicyService : IPolicyService private readonly ISavePolicyCommand _savePolicyCommand; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; + private readonly ICurrentContext _currentContext; public PolicyService( IApplicationCacheService applicationCacheService, @@ -48,7 +51,8 @@ public class PolicyService : IPolicyService IFeatureService featureService, ISavePolicyCommand savePolicyCommand, IRemoveOrganizationUserCommand removeOrganizationUserCommand, - IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery) + IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery, + ICurrentContext currentContext) { _applicationCacheService = applicationCacheService; _eventService = eventService; @@ -63,19 +67,24 @@ public class PolicyService : IPolicyService _savePolicyCommand = savePolicyCommand; _removeOrganizationUserCommand = removeOrganizationUserCommand; _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; + _currentContext = currentContext; } - public async Task SaveAsync(Policy policy, Guid? savingUserId) + public async Task SaveAsync(Policy policy, Guid? savingUserId, EventSystemUser? eventSystemUser = null) { if (_featureService.IsEnabled(FeatureFlagKeys.Pm13322AddPolicyDefinitions)) { // Transitional mapping - this will be moved to callers once the feature flag is removed + // TODO make sure to populate with SystemUser if not an actual user var policyUpdate = new PolicyUpdate { OrganizationId = policy.OrganizationId, Type = policy.Type, Enabled = policy.Enabled, - Data = policy.Data + Data = policy.Data, + PerformedBy = savingUserId.HasValue + ? new StandardUser(savingUserId.Value, await _currentContext.OrganizationOwner(policy.OrganizationId)) + : new SystemUser(eventSystemUser ?? EventSystemUser.Unknown) }; await _savePolicyCommand.SaveAsync(policyUpdate); diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForSingleOrgPolicy.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForSingleOrgPolicy.html.hbs new file mode 100644 index 0000000000..d04abe86c9 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForSingleOrgPolicy.html.hbs @@ -0,0 +1,14 @@ +{{#>FullHtmlLayout}} + + + + + + + +
+ Your user account has been revoked from the {{OrganizationName}} organization because your account is part of multiple organizations. Before you can re-join {{OrganizationName}}, you must first leave all other organizations. +
+ To leave an organization, first log into the web app, select the three dot menu next to the organization name, and select Leave. +
+{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForSingleOrgPolicy.text.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForSingleOrgPolicy.text.hbs new file mode 100644 index 0000000000..f933e8cf62 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForSingleOrgPolicy.text.hbs @@ -0,0 +1,5 @@ +{{#>BasicTextLayout}} +Your user account has been revoked from the {{OrganizationName}} organization because your account is part of multiple organizations. Before you can rejoin {{OrganizationName}}, you must first leave all other organizations. + +To leave an organization, first log in the web app (https://vault.bitwarden.com/#/login), select the three dot menu next to the organization name, and select Leave. +{{/BasicTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForTwoFactorPolicy.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForTwoFactorPolicy.html.hbs new file mode 100644 index 0000000000..cf38632a9e --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForTwoFactorPolicy.html.hbs @@ -0,0 +1,15 @@ +{{#>FullHtmlLayout}} + + + + + + + +
+ Your user account has been revoked from the {{OrganizationName}} organization because you do not have two-step login configured. Before you can re-join {{OrganizationName}}, you need to set up two-step login on your user account. +
+ Learn how to enable two-step login on your user account at + https://help.bitwarden.com/article/setup-two-step-login/ +
+{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForTwoFactorPolicy.text.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForTwoFactorPolicy.text.hbs new file mode 100644 index 0000000000..f197f37f00 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForTwoFactorPolicy.text.hbs @@ -0,0 +1,7 @@ +{{#>BasicTextLayout}} + Your user account has been removed from the {{OrganizationName}} organization because you do not have two-step login + configured. Before you can re-join this organization you need to set up two-step login on your user account. + + Learn how to enable two-step login on your user account at + https://help.bitwarden.com/article/setup-two-step-login/ +{{/BasicTextLayout}} diff --git a/src/Core/Models/Commands/CommandResult.cs b/src/Core/Models/Commands/CommandResult.cs new file mode 100644 index 0000000000..4ac2d62499 --- /dev/null +++ b/src/Core/Models/Commands/CommandResult.cs @@ -0,0 +1,12 @@ +namespace Bit.Core.Models.Commands; + +public class CommandResult(IEnumerable errors) +{ + public CommandResult(string error) : this([error]) { } + + public bool Success => ErrorMessages.Count == 0; + public bool HasErrors => ErrorMessages.Count > 0; + public List ErrorMessages { get; } = errors.ToList(); + + public CommandResult() : this([]) { } +} diff --git a/src/Core/Models/Mail/OrganizationUserRevokedForPolicySingleOrgViewModel.cs b/src/Core/Models/Mail/OrganizationUserRevokedForPolicySingleOrgViewModel.cs new file mode 100644 index 0000000000..27c784bd15 --- /dev/null +++ b/src/Core/Models/Mail/OrganizationUserRevokedForPolicySingleOrgViewModel.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Models.Mail; + +public class OrganizationUserRevokedForPolicySingleOrgViewModel : BaseMailModel +{ + public string OrganizationName { get; set; } +} diff --git a/src/Core/Models/Mail/OrganizationUserRevokedForPolicyTwoFactorViewModel.cs b/src/Core/Models/Mail/OrganizationUserRevokedForPolicyTwoFactorViewModel.cs new file mode 100644 index 0000000000..9286ee74b3 --- /dev/null +++ b/src/Core/Models/Mail/OrganizationUserRevokedForPolicyTwoFactorViewModel.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Models.Mail; + +public class OrganizationUserRevokedForPolicyTwoFactorViewModel : BaseMailModel +{ + public string OrganizationName { get; set; } +} diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index d11da2119a..96fdefcfad 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -91,6 +91,7 @@ public static class OrganizationServiceCollectionExtensions private static void AddOrganizationUserCommands(this IServiceCollection services) { services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 5514cd507d..bc8d1440f1 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -35,6 +35,8 @@ public interface IMailService Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable adminEmails, bool hasAccessSecretsManager = false); Task SendOrganizationConfirmedEmailAsync(string organizationName, string email, bool hasAccessSecretsManager = false); Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email); + Task SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(string organizationName, string email); + Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email); Task SendPasswordlessSignInAsync(string returnUrl, string token, string email); Task SendInvoiceUpcoming( string email, diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index e1943b0e3c..acc729e53c 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -25,8 +25,7 @@ public class HandlebarsMailService : IMailService private readonly GlobalSettings _globalSettings; private readonly IMailDeliveryService _mailDeliveryService; private readonly IMailEnqueuingService _mailEnqueuingService; - private readonly Dictionary> _templateCache = - new Dictionary>(); + private readonly Dictionary> _templateCache = new(); private bool _registeredHelpersAndPartials = false; @@ -295,6 +294,20 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(string organizationName, string email) + { + var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email); + var model = new OrganizationUserRevokedForPolicyTwoFactorViewModel + { + OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false), + WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, + SiteName = _globalSettings.SiteName + }; + await AddMessageContentAsync(message, "AdminConsole.OrganizationUserRevokedForTwoFactorPolicy", model); + message.Category = "OrganizationUserRevokedForTwoFactorPolicy"; + await _mailDeliveryService.SendEmailAsync(message); + } + public async Task SendWelcomeEmailAsync(User user) { var message = CreateDefaultMessage("Welcome to Bitwarden!", user.Email); @@ -496,6 +509,20 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email) + { + var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email); + var model = new OrganizationUserRevokedForPolicySingleOrgViewModel + { + OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false), + WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, + SiteName = _globalSettings.SiteName + }; + await AddMessageContentAsync(message, "AdminConsole.OrganizationUserRevokedForSingleOrgPolicy", model); + message.Category = "OrganizationUserRevokedForSingleOrgPolicy"; + await _mailDeliveryService.SendEmailAsync(message); + } + public async Task SendEnqueuedMailMessageAsync(IMailQueueMessage queueMessage) { var message = CreateDefaultMessage(queueMessage.Subject, queueMessage.ToEmails); diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index a56858fb96..399874eee7 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -79,6 +79,12 @@ public class NoopMailService : IMailService return Task.FromResult(0); } + public Task SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(string organizationName, string email) => + Task.CompletedTask; + + public Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email) => + Task.CompletedTask; + public Task SendTwoFactorEmailAsync(string email, string token) { return Task.FromResult(0); diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs index d5bdd3b6a2..42f79852f3 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -557,4 +557,14 @@ public class OrganizationUserRepository : Repository, IO return results.ToList(); } } + + public async Task RevokeManyByIdAsync(IEnumerable organizationUserIds) + { + await using var connection = new SqlConnection(ConnectionString); + + await connection.ExecuteAsync( + "[dbo].[OrganizationUser_SetStatusForUsersById]", + new { OrganizationUserIds = JsonSerializer.Serialize(organizationUserIds), Status = OrganizationUserStatusType.Revoked }, + commandType: CommandType.StoredProcedure); + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index a64c19704d..007ff1a7ff 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -721,4 +721,16 @@ public class OrganizationUserRepository : Repository organizationUserIds) + { + using var scope = ServiceScopeFactory.CreateScope(); + + var dbContext = GetDatabaseContext(scope); + + await dbContext.OrganizationUsers.Where(x => organizationUserIds.Contains(x.Id)) + .ExecuteUpdateAsync(s => s.SetProperty(x => x.Status, OrganizationUserStatusType.Revoked)); + + await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdsAsync(organizationUserIds); + } } diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_SetStatusForUsersById.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_SetStatusForUsersById.sql new file mode 100644 index 0000000000..95ed5a3155 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_SetStatusForUsersById.sql @@ -0,0 +1,29 @@ +CREATE PROCEDURE [dbo].[OrganizationUser_SetStatusForUsersById] + @OrganizationUserIds AS NVARCHAR(MAX), + @Status SMALLINT +AS +BEGIN + SET NOCOUNT ON + + -- Declare a table variable to hold the parsed JSON data + DECLARE @ParsedIds TABLE (Id UNIQUEIDENTIFIER); + + -- Parse the JSON input into the table variable + INSERT INTO @ParsedIds (Id) + SELECT value + FROM OPENJSON(@OrganizationUserIds); + + -- Check if the input table is empty + IF (SELECT COUNT(1) FROM @ParsedIds) < 1 + BEGIN + RETURN(-1); + END + + UPDATE + [dbo].[OrganizationUser] + SET [Status] = @Status + WHERE [Id] IN (SELECT Id from @ParsedIds) + + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @OrganizationUserIds +END + diff --git a/test/Core.Test/AdminConsole/AutoFixture/PolicyUpdateFixtures.cs b/test/Core.Test/AdminConsole/AutoFixture/PolicyUpdateFixtures.cs index dff9b57178..794f6fddf3 100644 --- a/test/Core.Test/AdminConsole/AutoFixture/PolicyUpdateFixtures.cs +++ b/test/Core.Test/AdminConsole/AutoFixture/PolicyUpdateFixtures.cs @@ -2,6 +2,7 @@ using AutoFixture; using AutoFixture.Xunit2; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; namespace Bit.Core.Test.AdminConsole.AutoFixture; @@ -12,7 +13,8 @@ internal class PolicyUpdateCustomization(PolicyType type, bool enabled) : ICusto { fixture.Customize(composer => composer .With(o => o.Type, type) - .With(o => o.Enabled, enabled)); + .With(o => o.Enabled, enabled) + .With(o => o.PerformedBy, new StandardUser(Guid.NewGuid(), false))); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs index 2fcaf8134c..8dbe533131 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; using Bit.Core.AdminConsole.Services; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -28,7 +29,12 @@ public class VerifyOrganizationDomainCommandTests DomainName = "Test Domain", Txt = "btw+test18383838383" }; + + sutProvider.GetDependency() + .UserId.Returns(Guid.NewGuid()); + expected.SetVerifiedDate(); + sutProvider.GetDependency() .GetByIdAsync(id) .Returns(expected); @@ -53,6 +59,10 @@ public class VerifyOrganizationDomainCommandTests sutProvider.GetDependency() .GetByIdAsync(id) .Returns(expected); + + sutProvider.GetDependency() + .UserId.Returns(Guid.NewGuid()); + sutProvider.GetDependency() .GetClaimedDomainsByDomainNameAsync(expected.DomainName) .Returns(new List { expected }); @@ -77,9 +87,14 @@ public class VerifyOrganizationDomainCommandTests sutProvider.GetDependency() .GetByIdAsync(id) .Returns(expected); + + sutProvider.GetDependency() + .UserId.Returns(Guid.NewGuid()); + sutProvider.GetDependency() .GetClaimedDomainsByDomainNameAsync(expected.DomainName) .Returns(new List()); + sutProvider.GetDependency() .ResolveAsync(expected.DomainName, Arg.Any()) .Returns(true); @@ -107,9 +122,14 @@ public class VerifyOrganizationDomainCommandTests sutProvider.GetDependency() .GetByIdAsync(id) .Returns(expected); + + sutProvider.GetDependency() + .UserId.Returns(Guid.NewGuid()); + sutProvider.GetDependency() .GetClaimedDomainsByDomainNameAsync(expected.DomainName) .Returns(new List()); + sutProvider.GetDependency() .ResolveAsync(expected.DomainName, Arg.Any()) .Returns(false); @@ -143,7 +163,7 @@ public class VerifyOrganizationDomainCommandTests [Theory, BitAutoData] public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeEnabled( - OrganizationDomain domain, SutProvider sutProvider) + OrganizationDomain domain, Guid userId, SutProvider sutProvider) { sutProvider.GetDependency() .GetClaimedDomainsByDomainNameAsync(domain.DomainName) @@ -157,11 +177,14 @@ public class VerifyOrganizationDomainCommandTests .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) .Returns(true); + sutProvider.GetDependency() + .UserId.Returns(userId); + _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); await sutProvider.GetDependency() .Received(1) - .SaveAsync(Arg.Is(x => x.Type == PolicyType.SingleOrg && x.OrganizationId == domain.OrganizationId && x.Enabled), null); + .SaveAsync(Arg.Is(x => x.Type == PolicyType.SingleOrg && x.OrganizationId == domain.OrganizationId && x.Enabled), userId); } [Theory, BitAutoData] @@ -176,6 +199,9 @@ public class VerifyOrganizationDomainCommandTests .ResolveAsync(domain.DomainName, domain.Txt) .Returns(true); + sutProvider.GetDependency() + .UserId.Returns(Guid.NewGuid()); + sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) .Returns(false); @@ -189,7 +215,7 @@ public class VerifyOrganizationDomainCommandTests [Theory, BitAutoData] public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldNotBeEnabled( - OrganizationDomain domain, SutProvider sutProvider) + OrganizationDomain domain, SutProvider sutProvider) { sutProvider.GetDependency() .GetClaimedDomainsByDomainNameAsync(domain.DomainName) @@ -199,6 +225,9 @@ public class VerifyOrganizationDomainCommandTests .ResolveAsync(domain.DomainName, domain.Txt) .Returns(false); + sutProvider.GetDependency() + .UserId.Returns(Guid.NewGuid()); + sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) .Returns(true); @@ -223,6 +252,9 @@ public class VerifyOrganizationDomainCommandTests .ResolveAsync(domain.DomainName, domain.Txt) .Returns(false); + sutProvider.GetDependency() + .UserId.Returns(Guid.NewGuid()); + sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) .Returns(true); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommandTests.cs new file mode 100644 index 0000000000..3653cd27d7 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommandTests.cs @@ -0,0 +1,185 @@ +using Bit.Core.AdminConsole.Models.Data; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers; + +[SutProviderCustomize] +public class RevokeNonCompliantOrganizationUserCommandTests +{ + [Theory, BitAutoData] + public async Task RevokeNonCompliantOrganizationUsersAsync_GivenUnrecognizedUserType_WhenAttemptingToRevoke_ThenErrorShouldBeReturned( + Guid organizationId, SutProvider sutProvider) + { + var command = new RevokeOrganizationUsersRequest(organizationId, [], new InvalidUser()); + + var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command); + + Assert.True(result.HasErrors); + Assert.Contains(RevokeNonCompliantOrganizationUserCommand.ErrorRequestedByWasNotValid, result.ErrorMessages); + } + + [Theory, BitAutoData] + public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserAttemptsToRevokeThemselves_ThenErrorShouldBeReturned( + Guid organizationId, OrganizationUserUserDetails revokingUser, + SutProvider sutProvider) + { + var command = new RevokeOrganizationUsersRequest(organizationId, revokingUser, + new StandardUser(revokingUser?.UserId ?? Guid.NewGuid(), true)); + + var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command); + + Assert.True(result.HasErrors); + Assert.Contains(RevokeNonCompliantOrganizationUserCommand.ErrorCannotRevokeSelf, result.ErrorMessages); + } + + [Theory, BitAutoData] + public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserAttemptsToRevokeOrgUsersFromAnotherOrg_ThenErrorShouldBeReturned( + Guid organizationId, OrganizationUserUserDetails userFromAnotherOrg, + SutProvider sutProvider) + { + userFromAnotherOrg.OrganizationId = Guid.NewGuid(); + + var command = new RevokeOrganizationUsersRequest(organizationId, userFromAnotherOrg, + new StandardUser(Guid.NewGuid(), true)); + + var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command); + + Assert.True(result.HasErrors); + Assert.Contains(RevokeNonCompliantOrganizationUserCommand.ErrorInvalidUsers, result.ErrorMessages); + } + + [Theory, BitAutoData] + public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserAttemptsToRevokeAllOwnersFromOrg_ThenErrorShouldBeReturned( + Guid organizationId, OrganizationUserUserDetails userToRevoke, + SutProvider sutProvider) + { + userToRevoke.OrganizationId = organizationId; + + var command = new RevokeOrganizationUsersRequest(organizationId, userToRevoke, + new StandardUser(Guid.NewGuid(), true)); + + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any>()) + .Returns(false); + + var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command); + + Assert.True(result.HasErrors); + Assert.Contains(RevokeNonCompliantOrganizationUserCommand.ErrorOrgMustHaveAtLeastOneOwner, result.ErrorMessages); + } + + [Theory, BitAutoData] + public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserAttemptsToRevokeOwnerWhenNotAnOwner_ThenErrorShouldBeReturned( + Guid organizationId, OrganizationUserUserDetails userToRevoke, + SutProvider sutProvider) + { + userToRevoke.OrganizationId = organizationId; + userToRevoke.Type = OrganizationUserType.Owner; + + var command = new RevokeOrganizationUsersRequest(organizationId, userToRevoke, + new StandardUser(Guid.NewGuid(), false)); + + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any>()) + .Returns(true); + + var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command); + + Assert.True(result.HasErrors); + Assert.Contains(RevokeNonCompliantOrganizationUserCommand.ErrorOnlyOwnersCanRevokeOtherOwners, result.ErrorMessages); + } + + [Theory, BitAutoData] + public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserAttemptsToRevokeUserWhoIsAlreadyRevoked_ThenErrorShouldBeReturned( + Guid organizationId, OrganizationUserUserDetails userToRevoke, + SutProvider sutProvider) + { + userToRevoke.OrganizationId = organizationId; + userToRevoke.Status = OrganizationUserStatusType.Revoked; + + var command = new RevokeOrganizationUsersRequest(organizationId, userToRevoke, + new StandardUser(Guid.NewGuid(), true)); + + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any>()) + .Returns(true); + + var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command); + + Assert.True(result.HasErrors); + Assert.Contains($"{RevokeNonCompliantOrganizationUserCommand.ErrorUserAlreadyRevoked} Id: {userToRevoke.Id}", result.ErrorMessages); + } + + [Theory, BitAutoData] + public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserHasMultipleInvalidUsers_ThenErrorShouldBeReturned( + Guid organizationId, IEnumerable usersToRevoke, + SutProvider sutProvider) + { + var revocableUsers = usersToRevoke.ToList(); + revocableUsers.ForEach(user => user.OrganizationId = organizationId); + revocableUsers[0].Type = OrganizationUserType.Owner; + revocableUsers[1].Status = OrganizationUserStatusType.Revoked; + + var command = new RevokeOrganizationUsersRequest(organizationId, revocableUsers, + new StandardUser(Guid.NewGuid(), false)); + + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any>()) + .Returns(true); + + var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command); + + Assert.True(result.HasErrors); + Assert.True(result.ErrorMessages.Count > 1); + } + + [Theory, BitAutoData] + public async Task RevokeNonCompliantOrganizationUsersAsync_GivenValidPopulatedRequest_WhenUserAttemptsToRevokeAUser_ThenUserShouldBeRevoked( + Guid organizationId, OrganizationUserUserDetails userToRevoke, + SutProvider sutProvider) + { + userToRevoke.OrganizationId = organizationId; + userToRevoke.Type = OrganizationUserType.Admin; + + var command = new RevokeOrganizationUsersRequest(organizationId, userToRevoke, + new StandardUser(Guid.NewGuid(), false)); + + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any>()) + .Returns(true); + + var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command); + + await sutProvider.GetDependency() + .Received(1) + .RevokeManyByIdAsync(Arg.Any>()); + + Assert.True(result.Success); + + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventsAsync( + Arg.Is>( + x => x.Any(y => + y.organizationUser.Id == userToRevoke.Id && y.eventType == EventType.OrganizationUser_Revoked) + )); + } + + public class InvalidUser : IActingUser + { + public Guid? UserId => Guid.Empty; + public bool IsOrganizationOwnerOrProvider => false; + public EventSystemUser? SystemUserType => null; + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs index 76ee574840..0731920757 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; using Bit.Core.Auth.Entities; @@ -10,6 +11,7 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Models.Commands; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; @@ -61,6 +63,79 @@ public class SingleOrgPolicyValidatorTests Assert.True(string.IsNullOrEmpty(result)); } + [Theory, BitAutoData] + public async Task OnSaveSideEffectsAsync_RevokesNonCompliantUsers( + [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg, false)] Policy policy, + Guid savingUserId, + Guid nonCompliantUserId, + Organization organization, SutProvider sutProvider) + { + policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; + + var compliantUser1 = new OrganizationUserUserDetails + { + OrganizationId = organization.Id, + Type = OrganizationUserType.User, + Status = OrganizationUserStatusType.Confirmed, + UserId = new Guid(), + Email = "user1@example.com" + }; + + var compliantUser2 = new OrganizationUserUserDetails + { + OrganizationId = organization.Id, + Type = OrganizationUserType.User, + Status = OrganizationUserStatusType.Confirmed, + UserId = new Guid(), + Email = "user2@example.com" + }; + + var nonCompliantUser = new OrganizationUserUserDetails + { + OrganizationId = organization.Id, + Type = OrganizationUserType.User, + Status = OrganizationUserStatusType.Confirmed, + UserId = nonCompliantUserId, + Email = "user3@example.com" + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) + .Returns([compliantUser1, compliantUser2, nonCompliantUser]); + + var otherOrganizationUser = new OrganizationUser + { + OrganizationId = new Guid(), + UserId = nonCompliantUserId, + Status = OrganizationUserStatusType.Confirmed + }; + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Is>(ids => ids.Contains(nonCompliantUserId))) + .Returns([otherOrganizationUser]); + + sutProvider.GetDependency().UserId.Returns(savingUserId); + sutProvider.GetDependency().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization); + + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + sutProvider.GetDependency() + .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()) + .Returns(new CommandResult()); + + await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy); + + await sutProvider.GetDependency() + .Received(1) + .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()); + await sutProvider.GetDependency() + .Received(1) + .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), + "user3@example.com"); + } + [Theory, BitAutoData] public async Task OnSaveSideEffectsAsync_RemovesNonCompliantUsers( [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate, @@ -116,6 +191,13 @@ public class SingleOrgPolicyValidatorTests sutProvider.GetDependency().UserId.Returns(savingUserId); sutProvider.GetDependency().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(false); + + sutProvider.GetDependency() + .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()) + .Returns(new CommandResult()); + await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy); await sutProvider.GetDependency() @@ -126,4 +208,73 @@ public class SingleOrgPolicyValidatorTests .SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), "user3@example.com"); } + + [Theory, BitAutoData] + public async Task OnSaveSideEffectsAsync_WhenAccountDeprovisioningIsEnabled_ThenUsersAreRevoked( + [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg, false)] Policy policy, + Guid savingUserId, + Guid nonCompliantUserId, + Organization organization, SutProvider sutProvider) + { + policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; + + var compliantUser1 = new OrganizationUserUserDetails + { + OrganizationId = organization.Id, + Type = OrganizationUserType.User, + Status = OrganizationUserStatusType.Confirmed, + UserId = new Guid(), + Email = "user1@example.com" + }; + + var compliantUser2 = new OrganizationUserUserDetails + { + OrganizationId = organization.Id, + Type = OrganizationUserType.User, + Status = OrganizationUserStatusType.Confirmed, + UserId = new Guid(), + Email = "user2@example.com" + }; + + var nonCompliantUser = new OrganizationUserUserDetails + { + OrganizationId = organization.Id, + Type = OrganizationUserType.User, + Status = OrganizationUserStatusType.Confirmed, + UserId = nonCompliantUserId, + Email = "user3@example.com" + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) + .Returns([compliantUser1, compliantUser2, nonCompliantUser]); + + var otherOrganizationUser = new OrganizationUser + { + OrganizationId = new Guid(), + UserId = nonCompliantUserId, + Status = OrganizationUserStatusType.Confirmed + }; + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Is>(ids => ids.Contains(nonCompliantUserId))) + .Returns([otherOrganizationUser]); + + sutProvider.GetDependency().UserId.Returns(savingUserId); + sutProvider.GetDependency().GetByIdAsync(policyUpdate.OrganizationId) + .Returns(organization); + + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); + + sutProvider.GetDependency() + .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()) + .Returns(new CommandResult()); + + await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy); + + await sutProvider.GetDependency() + .Received() + .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()); + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs index 4dce131749..4e5f1816a5 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs @@ -1,12 +1,14 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Commands; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; @@ -176,6 +178,10 @@ public class TwoFactorAuthenticationPolicyValidatorTests HasMasterPassword = false }; + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(false); + sutProvider.GetDependency() .GetManyDetailsByOrganizationAsync(policy.OrganizationId) .Returns(new List @@ -201,9 +207,151 @@ public class TwoFactorAuthenticationPolicyValidatorTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy)); - Assert.Contains("Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + Assert.Equal(TwoFactorAuthenticationPolicyValidator.NonCompliantMembersWillLoseAccessMessage, badRequestException.Message); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() .RemoveUserAsync(organizationId: default, organizationUserId: default, deletingUserId: default); } + + [Theory, BitAutoData] + public async Task OnSaveSideEffectsAsync_GivenUpdateTo2faPolicy_WhenAccountProvisioningIsDisabled_ThenRevokeUserCommandShouldNotBeCalled( + Organization organization, + [PolicyUpdate(PolicyType.TwoFactorAuthentication)] + PolicyUpdate policyUpdate, + [Policy(PolicyType.TwoFactorAuthentication, false)] + Policy policy, + SutProvider sutProvider) + { + policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(false); + + var orgUserDetailUserAcceptedWithout2Fa = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + Status = OrganizationUserStatusType.Accepted, + Type = OrganizationUserType.User, + Email = "user3@test.com", + Name = "TEST", + UserId = Guid.NewGuid(), + HasMasterPassword = true + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) + .Returns(new List + { + orgUserDetailUserAcceptedWithout2Fa + }); + + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>() + { + (orgUserDetailUserAcceptedWithout2Fa, false), + }); + + await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy); + + await sutProvider.GetDependency() + .DidNotReceive() + .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task OnSaveSideEffectsAsync_GivenUpdateTo2faPolicy_WhenAccountProvisioningIsEnabledAndUserDoesNotHaveMasterPassword_ThenNonCompliantMembersErrorMessageWillReturn( + Organization organization, + [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate, + [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy, + SutProvider sutProvider) + { + policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + var orgUserDetailUserWithout2Fa = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + Email = "user3@test.com", + Name = "TEST", + UserId = Guid.NewGuid(), + HasMasterPassword = false + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) + .Returns([orgUserDetailUserWithout2Fa]); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>() + { + (orgUserDetailUserWithout2Fa, false), + }); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy)); + + Assert.Equal(TwoFactorAuthenticationPolicyValidator.NonCompliantMembersWillLoseAccessMessage, exception.Message); + } + + [Theory, BitAutoData] + public async Task OnSaveSideEffectsAsync_WhenAccountProvisioningIsEnabledAndUserHasMasterPassword_ThenUserWillBeRevoked( + Organization organization, + [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate, + [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy, + SutProvider sutProvider) + { + policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + var orgUserDetailUserWithout2Fa = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + Email = "user3@test.com", + Name = "TEST", + UserId = Guid.NewGuid(), + HasMasterPassword = true + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) + .Returns([orgUserDetailUserWithout2Fa]); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>() + { + (orgUserDetailUserWithout2Fa, true), + }); + + sutProvider.GetDependency() + .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()) + .Returns(new CommandResult()); + + await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy); + + await sutProvider.GetDependency() + .Received(1) + .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()); + + await sutProvider.GetDependency() + .Received(1) + .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), + "user3@test.com"); + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs index 342ede9c82..3ca7004e70 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs @@ -100,7 +100,7 @@ public class SavePolicyCommandTests } [Theory, BitAutoData] - public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest(PolicyUpdate policyUpdate) + public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest([PolicyUpdate(PolicyType.ActivateAutofill)] PolicyUpdate policyUpdate) { var sutProvider = SutProviderFactory(); sutProvider.GetDependency() @@ -115,7 +115,7 @@ public class SavePolicyCommandTests } [Theory, BitAutoData] - public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest(PolicyUpdate policyUpdate) + public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest([PolicyUpdate(PolicyType.ActivateAutofill)] PolicyUpdate policyUpdate) { var sutProvider = SutProviderFactory(); sutProvider.GetDependency() diff --git a/test/Core.Test/AdminConsole/Services/EventServiceTests.cs b/test/Core.Test/AdminConsole/Services/EventServiceTests.cs index 18f5371b49..d064fce2ec 100644 --- a/test/Core.Test/AdminConsole/Services/EventServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventServiceTests.cs @@ -169,7 +169,6 @@ public class EventServiceTests new EventMessage() { IpAddress = ipAddress, - DeviceType = DeviceType.Server, OrganizationId = orgUser.OrganizationId, UserId = orgUser.UserId, OrganizationUserId = orgUser.Id, diff --git a/util/Migrator/DbScripts/2024-11-26-00_OrgUserSetStatusBulk.sql b/util/Migrator/DbScripts/2024-11-26-00_OrgUserSetStatusBulk.sql new file mode 100644 index 0000000000..95151f90de --- /dev/null +++ b/util/Migrator/DbScripts/2024-11-26-00_OrgUserSetStatusBulk.sql @@ -0,0 +1,28 @@ +CREATE PROCEDURE [dbo].[OrganizationUser_SetStatusForUsersById] + @OrganizationUserIds AS NVARCHAR(MAX), + @Status SMALLINT +AS +BEGIN + SET NOCOUNT ON + + -- Declare a table variable to hold the parsed JSON data + DECLARE @ParsedIds TABLE (Id UNIQUEIDENTIFIER); + + -- Parse the JSON input into the table variable + INSERT INTO @ParsedIds (Id) + SELECT value + FROM OPENJSON(@OrganizationUserIds); + + -- Check if the input table is empty + IF (SELECT COUNT(1) FROM @ParsedIds) < 1 + BEGIN + RETURN(-1); + END + + UPDATE + [dbo].[OrganizationUser] + SET [Status] = @Status + WHERE [Id] IN (SELECT Id from @ParsedIds) + + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @OrganizationUserIds +END From 674bd1e495b343e68174165cebb1f2458efcff0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 27 Nov 2024 12:26:42 +0000 Subject: [PATCH 17/94] [PM-13026] Refactor remove and bulkremove methods to throw error if user is managed by an organization (#5034) * Enhance RemoveOrganizationUserCommand to block removing managed users when account deprovisioning is enabled * Refactor RemoveUsersAsync method to return just the OrgUserId and update related logic. * Refactor RemoveOrganizationUserCommand to improve variable naming and remove unused logging method * Add support for event system user in RemoveUsersAsync method. Refactor unit tests. * Add xmldoc to IRemoveOrganizationUserCommand methods * Refactor RemoveOrganizationUserCommand to use TimeProvider for event date retrieval and update unit tests accordingly * Refactor RemoveOrganizationUserCommand to use constants for error messages * Refactor unit tests to separate feature flag tests * refactor: Update parameter names for clarity in RemoveOrganizationUserCommand * refactor: Rename validation and repository methods for user removal clarity --- .../OrganizationUsersController.cs | 2 +- .../IRemoveOrganizationUserCommand.cs | 49 +- .../RemoveOrganizationUserCommand.cs | 201 +++-- .../RemoveOrganizationUserCommandTests.cs | 777 ++++++++++++++---- 4 files changed, 804 insertions(+), 225 deletions(-) diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 3193962fa9..12d11fbc18 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -539,7 +539,7 @@ public class OrganizationUsersController : Controller var userId = _userService.GetProperUserId(User); var result = await _removeOrganizationUserCommand.RemoveUsersAsync(orgId, model.Ids, userId.Value); return new ListResponseModel(result.Select(r => - new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2))); + new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage))); } [RequireFeature(FeatureFlagKeys.AccountDeprovisioning)] diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs index 583645a890..7c1cdf05f8 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs @@ -1,14 +1,53 @@ -using Bit.Core.Entities; -using Bit.Core.Enums; +using Bit.Core.Enums; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; public interface IRemoveOrganizationUserCommand { + /// + /// Removes a user from an organization. + /// + /// The ID of the organization. + /// The ID of the user to remove. + Task RemoveUserAsync(Guid organizationId, Guid userId); + + /// + /// Removes a user from an organization with a specified deleting user. + /// + /// The ID of the organization. + /// The ID of the organization user to remove. + /// The ID of the user performing the removal operation. Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId); + /// + /// Removes a user from an organization using a system user. + /// + /// The ID of the organization. + /// The ID of the organization user to remove. + /// The system user performing the removal operation. Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser); - Task RemoveUserAsync(Guid organizationId, Guid userId); - Task>> RemoveUsersAsync(Guid organizationId, - IEnumerable organizationUserIds, Guid? deletingUserId); + + /// + /// Removes multiple users from an organization with a specified deleting user. + /// + /// The ID of the organization. + /// The collection of organization user IDs to remove. + /// The ID of the user performing the removal operation. + /// + /// A list of tuples containing the organization user id and the error message for each user that could not be removed, otherwise empty. + /// + Task> RemoveUsersAsync( + Guid organizationId, IEnumerable organizationUserIds, Guid? deletingUserId); + + /// + /// Removes multiple users from an organization using a system user. + /// + /// The ID of the organization. + /// The collection of organization user IDs to remove. + /// The system user performing the removal operation. + /// + /// A list of tuples containing the organization user id and the error message for each user that could not be removed, otherwise empty. + /// + Task> RemoveUsersAsync( + Guid organizationId, IEnumerable organizationUserIds, EventSystemUser eventSystemUser); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs index e6d56ea878..fa027f8e47 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs @@ -17,6 +17,16 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand private readonly IPushRegistrationService _pushRegistrationService; private readonly ICurrentContext _currentContext; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; + private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery; + private readonly IFeatureService _featureService; + private readonly TimeProvider _timeProvider; + + public const string UserNotFoundErrorMessage = "User not found."; + public const string UsersInvalidErrorMessage = "Users invalid."; + public const string RemoveYourselfErrorMessage = "You cannot remove yourself."; + public const string RemoveOwnerByNonOwnerErrorMessage = "Only owners can delete other owners."; + public const string RemoveLastConfirmedOwnerErrorMessage = "Organization must have at least one confirmed owner."; + public const string RemoveClaimedAccountErrorMessage = "Cannot remove member accounts claimed by the organization. To offboard a member, revoke or delete the account."; public RemoveOrganizationUserCommand( IDeviceRepository deviceRepository, @@ -25,7 +35,10 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand IPushNotificationService pushNotificationService, IPushRegistrationService pushRegistrationService, ICurrentContext currentContext, - IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, + IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery, + IFeatureService featureService, + TimeProvider timeProvider) { _deviceRepository = deviceRepository; _organizationUserRepository = organizationUserRepository; @@ -34,14 +47,27 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand _pushRegistrationService = pushRegistrationService; _currentContext = currentContext; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; + _getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery; + _featureService = featureService; + _timeProvider = timeProvider; + } + + public async Task RemoveUserAsync(Guid organizationId, Guid userId) + { + var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId); + ValidateRemoveUser(organizationId, organizationUser); + + await RepositoryRemoveUserAsync(organizationUser, deletingUserId: null, eventSystemUser: null); + + await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); } public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId) { var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); - ValidateDeleteUser(organizationId, organizationUser); + ValidateRemoveUser(organizationId, organizationUser); - await RepositoryDeleteUserAsync(organizationUser, deletingUserId); + await RepositoryRemoveUserAsync(organizationUser, deletingUserId, eventSystemUser: null); await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); } @@ -49,108 +75,79 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser) { var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); - ValidateDeleteUser(organizationId, organizationUser); + ValidateRemoveUser(organizationId, organizationUser); - await RepositoryDeleteUserAsync(organizationUser, null); + await RepositoryRemoveUserAsync(organizationUser, deletingUserId: null, eventSystemUser); await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); } - public async Task RemoveUserAsync(Guid organizationId, Guid userId) + public async Task> RemoveUsersAsync( + Guid organizationId, IEnumerable organizationUserIds, Guid? deletingUserId) { - var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId); - ValidateDeleteUser(organizationId, organizationUser); + var result = await RemoveUsersInternalAsync(organizationId, organizationUserIds, deletingUserId, eventSystemUser: null); - await RepositoryDeleteUserAsync(organizationUser, null); + var removedUsers = result.Where(r => string.IsNullOrEmpty(r.ErrorMessage)).Select(r => r.OrganizationUser).ToList(); + if (removedUsers.Any()) + { + DateTime? eventDate = _timeProvider.GetUtcNow().UtcDateTime; + await _eventService.LogOrganizationUserEventsAsync( + removedUsers.Select(ou => (ou, EventType.OrganizationUser_Removed, eventDate))); + } - await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); + return result.Select(r => (r.OrganizationUser.Id, r.ErrorMessage)); } - public async Task>> RemoveUsersAsync(Guid organizationId, - IEnumerable organizationUsersId, - Guid? deletingUserId) + public async Task> RemoveUsersAsync( + Guid organizationId, IEnumerable organizationUserIds, EventSystemUser eventSystemUser) { - var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId); - var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId) - .ToList(); + var result = await RemoveUsersInternalAsync(organizationId, organizationUserIds, deletingUserId: null, eventSystemUser); - if (!filteredUsers.Any()) + var removedUsers = result.Where(r => string.IsNullOrEmpty(r.ErrorMessage)).Select(r => r.OrganizationUser).ToList(); + if (removedUsers.Any()) { - throw new BadRequestException("Users invalid."); + DateTime? eventDate = _timeProvider.GetUtcNow().UtcDateTime; + await _eventService.LogOrganizationUserEventsAsync( + removedUsers.Select(ou => (ou, EventType.OrganizationUser_Removed, eventSystemUser, eventDate))); } - if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId)) - { - throw new BadRequestException("Organization must have at least one confirmed owner."); - } - - var deletingUserIsOwner = false; - if (deletingUserId.HasValue) - { - deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId); - } - - var result = new List>(); - var deletedUserIds = new List(); - foreach (var orgUser in filteredUsers) - { - try - { - if (deletingUserId.HasValue && orgUser.UserId == deletingUserId) - { - throw new BadRequestException("You cannot remove yourself."); - } - - if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue && !deletingUserIsOwner) - { - throw new BadRequestException("Only owners can delete other owners."); - } - - await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed); - - if (orgUser.UserId.HasValue) - { - await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value); - } - result.Add(Tuple.Create(orgUser, "")); - deletedUserIds.Add(orgUser.Id); - } - catch (BadRequestException e) - { - result.Add(Tuple.Create(orgUser, e.Message)); - } - - await _organizationUserRepository.DeleteManyAsync(deletedUserIds); - } - - return result; + return result.Select(r => (r.OrganizationUser.Id, r.ErrorMessage)); } - private void ValidateDeleteUser(Guid organizationId, OrganizationUser orgUser) + private void ValidateRemoveUser(Guid organizationId, OrganizationUser orgUser) { if (orgUser == null || orgUser.OrganizationId != organizationId) { - throw new NotFoundException("User not found."); + throw new NotFoundException(UserNotFoundErrorMessage); } } - private async Task RepositoryDeleteUserAsync(OrganizationUser orgUser, Guid? deletingUserId) + private async Task RepositoryRemoveUserAsync(OrganizationUser orgUser, Guid? deletingUserId, EventSystemUser? eventSystemUser) { if (deletingUserId.HasValue && orgUser.UserId == deletingUserId.Value) { - throw new BadRequestException("You cannot remove yourself."); + throw new BadRequestException(RemoveYourselfErrorMessage); } if (orgUser.Type == OrganizationUserType.Owner) { if (deletingUserId.HasValue && !await _currentContext.OrganizationOwner(orgUser.OrganizationId)) { - throw new BadRequestException("Only owners can delete other owners."); + throw new BadRequestException(RemoveOwnerByNonOwnerErrorMessage); } if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, new[] { orgUser.Id }, includeProvider: true)) { - throw new BadRequestException("Organization must have at least one confirmed owner."); + throw new BadRequestException(RemoveLastConfirmedOwnerErrorMessage); + } + } + + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null) + { + var managementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(orgUser.OrganizationId, new[] { orgUser.Id }); + if (managementStatus.TryGetValue(orgUser.Id, out var isManaged) && isManaged) + { + throw new BadRequestException(RemoveClaimedAccountErrorMessage); } } @@ -177,4 +174,70 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand organizationId.ToString()); await _pushNotificationService.PushSyncOrgKeysAsync(userId); } + + private async Task> RemoveUsersInternalAsync( + Guid organizationId, IEnumerable organizationUsersId, Guid? deletingUserId, EventSystemUser? eventSystemUser) + { + var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId); + var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId).ToList(); + + if (!filteredUsers.Any()) + { + throw new BadRequestException(UsersInvalidErrorMessage); + } + + if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId)) + { + throw new BadRequestException(RemoveLastConfirmedOwnerErrorMessage); + } + + var deletingUserIsOwner = false; + if (deletingUserId.HasValue) + { + deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId); + } + + var managementStatus = _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null + ? await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, filteredUsers.Select(u => u.Id)) + : filteredUsers.ToDictionary(u => u.Id, u => false); + var result = new List<(OrganizationUser OrganizationUser, string ErrorMessage)>(); + foreach (var orgUser in filteredUsers) + { + try + { + if (deletingUserId.HasValue && orgUser.UserId == deletingUserId) + { + throw new BadRequestException(RemoveYourselfErrorMessage); + } + + if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue && !deletingUserIsOwner) + { + throw new BadRequestException(RemoveOwnerByNonOwnerErrorMessage); + } + + if (managementStatus.TryGetValue(orgUser.Id, out var isManaged) && isManaged) + { + throw new BadRequestException(RemoveClaimedAccountErrorMessage); + } + + result.Add((orgUser, string.Empty)); + } + catch (BadRequestException e) + { + result.Add((orgUser, e.Message)); + } + } + + var organizationUsersToRemove = result.Where(r => string.IsNullOrEmpty(r.ErrorMessage)).Select(r => r.OrganizationUser).ToList(); + if (organizationUsersToRemove.Any()) + { + await _organizationUserRepository.DeleteManyAsync(organizationUsersToRemove.Select(ou => ou.Id)); + foreach (var orgUser in organizationUsersToRemove.Where(ou => ou.UserId.HasValue)) + { + await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value); + } + } + + return result; + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs index 2d10ce626b..61371b756e 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs @@ -9,6 +9,7 @@ using Bit.Core.Services; using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; using NSubstitute; using Xunit; @@ -18,38 +19,93 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers; public class RemoveOrganizationUserCommandTests { [Theory, BitAutoData] - public async Task RemoveUser_Success( + public async Task RemoveUser_WithDeletingUserId_Success( [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser deletingUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - var currentContext = sutProvider.GetDependency(); - + // Arrange organizationUser.OrganizationId = deletingUser.OrganizationId; - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); - currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + sutProvider.GetDependency() + .GetByIdAsync(deletingUser.Id) + .Returns(deletingUser); + sutProvider.GetDependency() + .OrganizationOwner(deletingUser.OrganizationId) + .Returns(true); + + // Act await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId); - await organizationUserRepository.Received(1).DeleteAsync(organizationUser); - await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); + // Assert + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetUsersOrganizationManagementStatusAsync(default, default); + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(organizationUser); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); } - [Theory] - [BitAutoData] - public async Task RemoveUser_NotFound_ThrowsException(SutProvider sutProvider, + [Theory, BitAutoData] + public async Task RemoveUser_WithDeletingUserId_WithAccountDeprovisioningEnabled_Success( + [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser deletingUser, + SutProvider sutProvider) + { + // Arrange + organizationUser.OrganizationId = deletingUser.OrganizationId; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + sutProvider.GetDependency() + .GetByIdAsync(deletingUser.Id) + .Returns(deletingUser); + sutProvider.GetDependency() + .OrganizationOwner(deletingUser.OrganizationId) + .Returns(true); + + // Act + await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .GetUsersOrganizationManagementStatusAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id))); + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(organizationUser); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); + } + + [Theory, BitAutoData] + public async Task RemoveUser_WithDeletingUserId_NotFound_ThrowsException( + SutProvider sutProvider, Guid organizationId, Guid organizationUserId) { - await Assert.ThrowsAsync(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null)); + // Act & Assert + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null)); } - [Theory] - [BitAutoData] - public async Task RemoveUser_MismatchingOrganizationId_ThrowsException( + [Theory, BitAutoData] + public async Task RemoveUser_WithDeletingUserId_MismatchingOrganizationId_ThrowsException( SutProvider sutProvider, Guid organizationId, Guid organizationUserId) { + // Arrange sutProvider.GetDependency() .GetByIdAsync(organizationUserId) .Returns(new OrganizationUser @@ -58,92 +114,231 @@ public class RemoveOrganizationUserCommandTests OrganizationId = Guid.NewGuid() }); - await Assert.ThrowsAsync(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null)); + // Act & Assert + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null)); } [Theory, BitAutoData] - public async Task RemoveUser_InvalidUser_ThrowsException( - OrganizationUser organizationUser, OrganizationUser deletingUser, - SutProvider sutProvider) + public async Task RemoveUser_WithDeletingUserId_InvalidUser_ThrowsException( + OrganizationUser organizationUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); + // Arrange + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + // Act & Assert var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.Id, deletingUser.UserId)); - Assert.Contains("User not found.", exception.Message); + () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.Id, null)); + Assert.Contains(RemoveOrganizationUserCommand.UserNotFoundErrorMessage, exception.Message); } [Theory, BitAutoData] - public async Task RemoveUser_RemoveYourself_ThrowsException(OrganizationUser deletingUser, SutProvider sutProvider) + public async Task RemoveUser_WithDeletingUserId_RemoveYourself_ThrowsException( + OrganizationUser deletingUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - - organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); + // Arrange + sutProvider.GetDependency() + .GetByIdAsync(deletingUser.Id) + .Returns(deletingUser); + // Act & Assert var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, deletingUser.Id, deletingUser.UserId)); - Assert.Contains("You cannot remove yourself.", exception.Message); + Assert.Contains(RemoveOrganizationUserCommand.RemoveYourselfErrorMessage, exception.Message); } [Theory, BitAutoData] - public async Task RemoveUser_NonOwnerRemoveOwner_ThrowsException( + public async Task RemoveUser_WithDeletingUserId_NonOwnerRemoveOwner_ThrowsException( [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, [OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - var currentContext = sutProvider.GetDependency(); - + // Arrange organizationUser.OrganizationId = deletingUser.OrganizationId; - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - currentContext.OrganizationAdmin(deletingUser.OrganizationId).Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + sutProvider.GetDependency() + .OrganizationAdmin(organizationUser.OrganizationId) + .Returns(true); + + // Act & Assert var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId)); - Assert.Contains("Only owners can delete other owners.", exception.Message); + () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUser.UserId)); + Assert.Contains(RemoveOrganizationUserCommand.RemoveOwnerByNonOwnerErrorMessage, exception.Message); } [Theory, BitAutoData] - public async Task RemoveUser_RemovingLastOwner_ThrowsException( + public async Task RemoveUser_WithDeletingUserId_RemovingLastOwner_ThrowsException( [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, OrganizationUser deletingUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - var hasConfirmedOwnersExceptQuery = sutProvider.GetDependency(); - + // Arrange organizationUser.OrganizationId = deletingUser.OrganizationId; - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync( - deletingUser.OrganizationId, - Arg.Is>(i => i.Contains(organizationUser.Id)), Arg.Any()) - .Returns(false); + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), + Arg.Any()) + .Returns(false); + sutProvider.GetDependency() + .OrganizationOwner(deletingUser.OrganizationId) + .Returns(true); + + // Act & Assert var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, null)); - Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); - hasConfirmedOwnersExceptQuery + () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUser.UserId)); + Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message); + await sutProvider.GetDependency() .Received(1) .HasConfirmedOwnersExceptAsync( organizationUser.OrganizationId, Arg.Is>(i => i.Contains(organizationUser.Id)), true); } + [Theory, BitAutoData] + public async Task RemoveUserAsync_WithDeletingUserId_WithAccountDeprovisioningEnabled_WhenUserIsManaged_ThrowsException( + [OrganizationUser(status: OrganizationUserStatusType.Confirmed)] OrganizationUser orgUser, + Guid deletingUserId, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(orgUser.Id) + .Returns(orgUser); + sutProvider.GetDependency() + .GetUsersOrganizationManagementStatusAsync(orgUser.OrganizationId, Arg.Is>(i => i.Contains(orgUser.Id))) + .Returns(new Dictionary { { orgUser.Id, true } }); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUserAsync(orgUser.OrganizationId, orgUser.Id, deletingUserId)); + Assert.Contains(RemoveOrganizationUserCommand.RemoveClaimedAccountErrorMessage, exception.Message); + await sutProvider.GetDependency() + .Received(1) + .GetUsersOrganizationManagementStatusAsync(orgUser.OrganizationId, Arg.Is>(i => i.Contains(orgUser.Id))); + } + [Theory, BitAutoData] public async Task RemoveUser_WithEventSystemUser_Success( [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, + EventSystemUser eventSystemUser, SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + + // Act + await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser); + + // Assert + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetUsersOrganizationManagementStatusAsync(default, default); + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(organizationUser); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); + } + + [Theory, BitAutoData] + public async Task RemoveUser_WithEventSystemUser_WithAccountDeprovisioningEnabled_Success( + [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, + EventSystemUser eventSystemUser, SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + + // Act + await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser); + + // Assert + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetUsersOrganizationManagementStatusAsync(default, default); + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(organizationUser); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); + } + + [Theory] + [BitAutoData] + public async Task RemoveUser_WithEventSystemUser_NotFound_ThrowsException( + SutProvider sutProvider, + Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser) + { + // Act & Assert + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, eventSystemUser)); + } + + [Theory] + [BitAutoData] + public async Task RemoveUser_WithEventSystemUser_MismatchingOrganizationId_ThrowsException( + SutProvider sutProvider, Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser) + { + // Arrange + sutProvider.GetDependency() + .GetByIdAsync(organizationUserId) + .Returns(new OrganizationUser + { + Id = organizationUserId, + OrganizationId = Guid.NewGuid() + }); + + // Act & Assert + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, eventSystemUser)); + } + + [Theory, BitAutoData] + public async Task RemoveUser_WithEventSystemUser_RemovingLastOwner_ThrowsException( + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); + // Arrange + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), + Arg.Any()) + .Returns(false); - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - - await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser); - - await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser)); + Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message); + await sutProvider.GetDependency() + .Received(1) + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), true); } [Theory, BitAutoData] @@ -170,23 +365,26 @@ public class RemoveOrganizationUserCommandTests } [Theory, BitAutoData] - public async Task RemoveUser_ByUserId_NotFound_ThrowsException(SutProvider sutProvider, - Guid organizationId, Guid userId) + public async Task RemoveUser_ByUserId_NotFound_ThrowsException( + SutProvider sutProvider, Guid organizationId, Guid userId) { + // Act & Assert await Assert.ThrowsAsync(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, userId)); } [Theory, BitAutoData] - public async Task RemoveUser_ByUserId_InvalidUser_ThrowsException(OrganizationUser organizationUser, - SutProvider sutProvider) + public async Task RemoveUser_ByUserId_InvalidUser_ThrowsException( + OrganizationUser organizationUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - - organizationUserRepository.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value).Returns(organizationUser); + // Arrange + sutProvider.GetDependency() + .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value) + .Returns(organizationUser); + // Act & Assert var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.UserId.Value)); - Assert.Contains("User not found.", exception.Message); + Assert.Contains(RemoveOrganizationUserCommand.UserNotFoundErrorMessage, exception.Message); } [Theory, BitAutoData] @@ -194,21 +392,22 @@ public class RemoveOrganizationUserCommandTests [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - var hasConfirmedOwnersExceptQuery = sutProvider.GetDependency(); - - organizationUserRepository.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value).Returns(organizationUser); - hasConfirmedOwnersExceptQuery + // Arrange + sutProvider.GetDependency() + .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value) + .Returns(organizationUser); + sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync( organizationUser.OrganizationId, Arg.Is>(i => i.Contains(organizationUser.Id)), Arg.Any()) .Returns(false); + // Act & Assert var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value)); - Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); - hasConfirmedOwnersExceptQuery + Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message); + await sutProvider.GetDependency() .Received(1) .HasConfirmedOwnersExceptAsync( organizationUser.OrganizationId, @@ -217,93 +416,371 @@ public class RemoveOrganizationUserCommandTests } [Theory, BitAutoData] - public async Task RemoveUsers_FilterInvalid_ThrowsException(OrganizationUser organizationUser, OrganizationUser deletingUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var organizationUsers = new[] { organizationUser }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId)); - Assert.Contains("Users invalid.", exception.Message); - } - - [Theory, BitAutoData] - public async Task RemoveUsers_RemoveYourself_ThrowsException( - OrganizationUser deletingUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var organizationUsers = new[] { deletingUser }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - sutProvider.GetDependency() - .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) - .Returns(true); - - var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); - Assert.Contains("You cannot remove yourself.", result[0].Item2); - } - - [Theory, BitAutoData] - public async Task RemoveUsers_NonOwnerRemoveOwner_ThrowsException( - [OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser, - [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, - [OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser orgUser2, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - - orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; - var organizationUsers = new[] { orgUser1 }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - sutProvider.GetDependency() - .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) - .Returns(true); - - var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); - Assert.Contains("Only owners can delete other owners.", result[0].Item2); - } - - [Theory, BitAutoData] - public async Task RemoveUsers_LastOwner_ThrowsException( - [OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - - var organizationUsers = new[] { orgUser }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - organizationUserRepository.GetManyByOrganizationAsync(orgUser.OrganizationId, OrganizationUserType.Owner).Returns(organizationUsers); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, organizationUserIds, null)); - Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); - } - - [Theory, BitAutoData] - public async Task RemoveUsers_Success( + public async Task RemoveUsers_WithDeletingUserId_Success( [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser, - [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2, - SutProvider sutProvider) + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2) { - var organizationUserRepository = sutProvider.GetDependency(); - var currentContext = sutProvider.GetDependency(); - + // Arrange + var sutProvider = SutProviderFactory(); + var eventDate = sutProvider.GetDependency().GetUtcNow().UtcDateTime; orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; var organizationUsers = new[] { orgUser1, orgUser2 }; var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); + + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .GetByIdAsync(deletingUser.Id) + .Returns(deletingUser); sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) .Returns(true); - currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true); + sutProvider.GetDependency() + .OrganizationOwner(deletingUser.OrganizationId) + .Returns(true); + sutProvider.GetDependency() + .GetUsersOrganizationManagementStatusAsync( + deletingUser.OrganizationId, + Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))) + .Returns(new Dictionary { { orgUser1.Id, false }, { orgUser2.Id, false } }); - await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); + // Act + var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); + + // Assert + Assert.Equal(2, result.Count()); + Assert.All(result, r => Assert.Empty(r.ErrorMessage)); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetUsersOrganizationManagementStatusAsync(default, default); + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync(Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventsAsync( + Arg.Is>(i => + i.First().OrganizationUser.Id == orgUser1.Id + && i.Last().OrganizationUser.Id == orgUser2.Id + && i.All(u => u.DateTime == eventDate))); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_WithDeletingUserId_WithAccountDeprovisioningEnabled_Success( + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser, + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2) + { + // Arrange + var sutProvider = SutProviderFactory(); + var eventDate = sutProvider.GetDependency().GetUtcNow().UtcDateTime; + orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; + var organizationUsers = new[] { orgUser1, orgUser2 }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .GetByIdAsync(deletingUser.Id) + .Returns(deletingUser); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) + .Returns(true); + sutProvider.GetDependency() + .OrganizationOwner(deletingUser.OrganizationId) + .Returns(true); + sutProvider.GetDependency() + .GetUsersOrganizationManagementStatusAsync( + deletingUser.OrganizationId, + Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))) + .Returns(new Dictionary { { orgUser1.Id, false }, { orgUser2.Id, false } }); + + // Act + var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); + + // Assert + Assert.Equal(2, result.Count()); + Assert.All(result, r => Assert.Empty(r.ErrorMessage)); + await sutProvider.GetDependency() + .Received(1) + .GetUsersOrganizationManagementStatusAsync( + deletingUser.OrganizationId, + Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))); + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync(Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventsAsync( + Arg.Is>(i => + i.First().OrganizationUser.Id == orgUser1.Id + && i.Last().OrganizationUser.Id == orgUser2.Id + && i.All(u => u.DateTime == eventDate))); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_WithDeletingUserId_WithMismatchingOrganizationId_ThrowsException(OrganizationUser organizationUser, + OrganizationUser deletingUser, SutProvider sutProvider) + { + // Arrange + var organizationUsers = new[] { organizationUser }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId)); + Assert.Contains(RemoveOrganizationUserCommand.UsersInvalidErrorMessage, exception.Message); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_WithDeletingUserId_RemoveYourself_ThrowsException( + OrganizationUser deletingUser, SutProvider sutProvider) + { + // Arrange + var organizationUsers = new[] { deletingUser }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) + .Returns(true); + + // Act + var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); + + // Assert + Assert.Contains(RemoveOrganizationUserCommand.RemoveYourselfErrorMessage, result.First().ErrorMessage); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_WithDeletingUserId_NonOwnerRemoveOwner_ThrowsException( + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, + [OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser orgUser2, + [OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser, + SutProvider sutProvider) + { + // Arrange + orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; + var organizationUsers = new[] { orgUser1, orgUser2 }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) + .Returns(true); + + // Act + var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); + + // Assert + Assert.Contains(RemoveOrganizationUserCommand.RemoveOwnerByNonOwnerErrorMessage, result.First().ErrorMessage); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_WithDeletingUserId_RemovingManagedUser_WithAccountDeprovisioningEnabled_ThrowsException( + [OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser, + OrganizationUser deletingUser, + SutProvider sutProvider) + { + // Arrange + orgUser.OrganizationId = deletingUser.OrganizationId; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + sutProvider.GetDependency() + .GetManyAsync(Arg.Is>(i => i.Contains(orgUser.Id))) + .Returns(new[] { orgUser }); + + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, Arg.Any>()) + .Returns(true); + + sutProvider.GetDependency() + .GetUsersOrganizationManagementStatusAsync(orgUser.OrganizationId, Arg.Is>(i => i.Contains(orgUser.Id))) + .Returns(new Dictionary { { orgUser.Id, true } }); + + // Act + var result = await sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUser.UserId); + + // Assert + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventsAsync(Arg.Any>()); + Assert.Contains(RemoveOrganizationUserCommand.RemoveClaimedAccountErrorMessage, result.First().ErrorMessage); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_WithDeletingUserId_LastOwner_ThrowsException( + [OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser, + SutProvider sutProvider) + { + // Arrange + var organizationUsers = new[] { orgUser }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .GetManyByOrganizationAsync(orgUser.OrganizationId, OrganizationUserType.Owner) + .Returns(organizationUsers); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, organizationUserIds, null)); + Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_WithEventSystemUser_Success( + EventSystemUser eventSystemUser, + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, + OrganizationUser orgUser2) + { + // Arrange + var sutProvider = SutProviderFactory(); + var eventDate = sutProvider.GetDependency().GetUtcNow().UtcDateTime; + orgUser1.OrganizationId = orgUser2.OrganizationId; + var organizationUsers = new[] { orgUser1, orgUser2 }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(orgUser1.OrganizationId, Arg.Any>()) + .Returns(true); + + // Act + var result = await sutProvider.Sut.RemoveUsersAsync(orgUser1.OrganizationId, organizationUserIds, eventSystemUser); + + // Assert + Assert.Equal(2, result.Count()); + Assert.All(result, r => Assert.Empty(r.ErrorMessage)); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetUsersOrganizationManagementStatusAsync(default, default); + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync(Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventsAsync( + Arg.Is>( + i => i.First().OrganizationUser.Id == orgUser1.Id + && i.Last().OrganizationUser.Id == orgUser2.Id + && i.All(u => u.EventSystemUser == eventSystemUser + && u.DateTime == eventDate))); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_WithEventSystemUser_WithAccountDeprovisioningEnabled_Success( + EventSystemUser eventSystemUser, + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, + OrganizationUser orgUser2) + { + // Arrange + var sutProvider = SutProviderFactory(); + var eventDate = sutProvider.GetDependency().GetUtcNow().UtcDateTime; + orgUser1.OrganizationId = orgUser2.OrganizationId; + var organizationUsers = new[] { orgUser1, orgUser2 }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(orgUser1.OrganizationId, Arg.Any>()) + .Returns(true); + + // Act + var result = await sutProvider.Sut.RemoveUsersAsync(orgUser1.OrganizationId, organizationUserIds, eventSystemUser); + + // Assert + Assert.Equal(2, result.Count()); + Assert.All(result, r => Assert.Empty(r.ErrorMessage)); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetUsersOrganizationManagementStatusAsync(default, default); + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync(Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventsAsync( + Arg.Is>( + i => i.First().OrganizationUser.Id == orgUser1.Id + && i.Last().OrganizationUser.Id == orgUser2.Id + && i.All(u => u.EventSystemUser == eventSystemUser + && u.DateTime == eventDate))); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_WithEventSystemUser_WithMismatchingOrganizationId_ThrowsException( + EventSystemUser eventSystemUser, + [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, + SutProvider sutProvider) + { + // Arrange + var organizationUsers = new[] { organizationUser }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUsersAsync(Guid.NewGuid(), organizationUserIds, eventSystemUser)); + Assert.Contains(RemoveOrganizationUserCommand.UsersInvalidErrorMessage, exception.Message); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_WithEventSystemUser_LastOwner_ThrowsException( + [OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser, + EventSystemUser eventSystemUser, SutProvider sutProvider) + { + // Arrange + var organizationUsers = new[] { orgUser }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + + sutProvider.GetDependency() + .GetManyAsync(default) + .ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .GetManyByOrganizationAsync(orgUser.OrganizationId, OrganizationUserType.Owner) + .Returns(organizationUsers); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, organizationUserIds, eventSystemUser)); + Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message); + } + + /// + /// Returns a new SutProvider with a FakeTimeProvider registered in the Sut. + /// + private static SutProvider SutProviderFactory() + { + return new SutProvider() + .WithFakeTimeProvider() + .Create(); } } From c8930d44f26a1003cae1f803215ea61653ab2347 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Wed, 27 Nov 2024 06:47:18 -0600 Subject: [PATCH 18/94] Swapping [] for Array.Empty (#5092) --- src/Core/Models/Commands/CommandResult.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Models/Commands/CommandResult.cs b/src/Core/Models/Commands/CommandResult.cs index 4ac2d62499..9e5d91e09c 100644 --- a/src/Core/Models/Commands/CommandResult.cs +++ b/src/Core/Models/Commands/CommandResult.cs @@ -8,5 +8,5 @@ public class CommandResult(IEnumerable errors) public bool HasErrors => ErrorMessages.Count > 0; public List ErrorMessages { get; } = errors.ToList(); - public CommandResult() : this([]) { } + public CommandResult() : this(Array.Empty()) { } } From aa364cacef9cd0e3bc3746497d0ed6593143589b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:49:20 +0000 Subject: [PATCH 19/94] [PM-14876] Update admin panel copy from 'Domain Verified' to 'Claimed Account' and rename associated ViewModel properties (#5058) --- src/Admin/Models/UserEditModel.cs | 10 +++------- src/Admin/Models/UserViewModel.cs | 14 +++++++------- src/Admin/Views/Users/_ViewInformation.cshtml | 7 ++++--- test/Admin.Test/Models/UserViewModelTests.cs | 14 ++++---------- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/Admin/Models/UserEditModel.cs b/src/Admin/Models/UserEditModel.cs index 2ad0b27cbd..ed2d653246 100644 --- a/src/Admin/Models/UserEditModel.cs +++ b/src/Admin/Models/UserEditModel.cs @@ -9,10 +9,7 @@ namespace Bit.Admin.Models; public class UserEditModel { - public UserEditModel() - { - - } + public UserEditModel() { } public UserEditModel( User user, @@ -21,10 +18,9 @@ public class UserEditModel BillingInfo billingInfo, BillingHistoryInfo billingHistoryInfo, GlobalSettings globalSettings, - bool? domainVerified - ) + bool? claimedAccount) { - User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, domainVerified); + User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, claimedAccount); BillingInfo = billingInfo; BillingHistoryInfo = billingHistoryInfo; diff --git a/src/Admin/Models/UserViewModel.cs b/src/Admin/Models/UserViewModel.cs index 75c089ee5f..7fddbc0f54 100644 --- a/src/Admin/Models/UserViewModel.cs +++ b/src/Admin/Models/UserViewModel.cs @@ -14,7 +14,7 @@ public class UserViewModel public bool Premium { get; } public short? MaxStorageGb { get; } public bool EmailVerified { get; } - public bool? DomainVerified { get; } + public bool? ClaimedAccount { get; } public bool TwoFactorEnabled { get; } public DateTime AccountRevisionDate { get; } public DateTime RevisionDate { get; } @@ -36,7 +36,7 @@ public class UserViewModel bool premium, short? maxStorageGb, bool emailVerified, - bool? domainVerified, + bool? claimedAccount, bool twoFactorEnabled, DateTime accountRevisionDate, DateTime revisionDate, @@ -58,7 +58,7 @@ public class UserViewModel Premium = premium; MaxStorageGb = maxStorageGb; EmailVerified = emailVerified; - DomainVerified = domainVerified; + ClaimedAccount = claimedAccount; TwoFactorEnabled = twoFactorEnabled; AccountRevisionDate = accountRevisionDate; RevisionDate = revisionDate; @@ -79,7 +79,7 @@ public class UserViewModel users.Select(user => MapViewModel(user, lookup, false)); public static UserViewModel MapViewModel(User user, - IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup, bool? domainVerified) => + IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup, bool? claimedAccount) => new( user.Id, user.Name, @@ -89,7 +89,7 @@ public class UserViewModel user.Premium, user.MaxStorageGb, user.EmailVerified, - domainVerified, + claimedAccount, IsTwoFactorEnabled(user, lookup), user.AccountRevisionDate, user.RevisionDate, @@ -106,7 +106,7 @@ public class UserViewModel public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) => MapViewModel(user, isTwoFactorEnabled, Array.Empty(), false); - public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable ciphers, bool? domainVerified) => + public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable ciphers, bool? claimedAccount) => new( user.Id, user.Name, @@ -116,7 +116,7 @@ public class UserViewModel user.Premium, user.MaxStorageGb, user.EmailVerified, - domainVerified, + claimedAccount, isTwoFactorEnabled, user.AccountRevisionDate, user.RevisionDate, diff --git a/src/Admin/Views/Users/_ViewInformation.cshtml b/src/Admin/Views/Users/_ViewInformation.cshtml index 00afcc19df..ae8bcb0737 100644 --- a/src/Admin/Views/Users/_ViewInformation.cshtml +++ b/src/Admin/Views/Users/_ViewInformation.cshtml @@ -12,9 +12,10 @@
Email Verified
@(Model.EmailVerified ? "Yes" : "No")
- @if(Model.DomainVerified.HasValue){ -
Domain Verified
-
@(Model.DomainVerified.Value == true ? "Yes" : "No")
+ @if(Model.ClaimedAccount.HasValue) + { +
Claimed Account
+
@(Model.ClaimedAccount.Value ? "Yes" : "No")
}
Using 2FA
diff --git a/test/Admin.Test/Models/UserViewModelTests.cs b/test/Admin.Test/Models/UserViewModelTests.cs index fac5d5f0eb..d015b98328 100644 --- a/test/Admin.Test/Models/UserViewModelTests.cs +++ b/test/Admin.Test/Models/UserViewModelTests.cs @@ -1,6 +1,4 @@ -#nullable enable - -using Bit.Admin.Models; +using Bit.Admin.Models; using Bit.Core.Entities; using Bit.Core.Vault.Entities; using Bit.Test.Common.AutoFixture.Attributes; @@ -116,30 +114,26 @@ public class UserViewModelTests var actual = UserViewModel.MapViewModel(user, true, Array.Empty(), verifiedDomain); - Assert.True(actual.DomainVerified); + Assert.True(actual.ClaimedAccount); } [Theory] [BitAutoData] public void MapUserViewModel_WithoutVerifiedDomain_ReturnsUserViewModel(User user) { - var verifiedDomain = false; var actual = UserViewModel.MapViewModel(user, true, Array.Empty(), verifiedDomain); - Assert.False(actual.DomainVerified); + Assert.False(actual.ClaimedAccount); } [Theory] [BitAutoData] public void MapUserViewModel_WithNullVerifiedDomain_ReturnsUserViewModel(User user) { - var actual = UserViewModel.MapViewModel(user, true, Array.Empty(), null); - Assert.Null(actual.DomainVerified); + Assert.Null(actual.ClaimedAccount); } - - } From e9297f85e9593abafb870f833dfcfad4c67e21ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:55:05 +0000 Subject: [PATCH 20/94] [PM-12684] Remove deprecated feature flag for Members TwoFA query optimization (#5076) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 8fe3886539..8077494794 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -129,7 +129,6 @@ public static class FeatureFlagKeys public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor"; public const string DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2"; - public const string MembersTwoFAQueryOptimization = "ac-1698-members-two-fa-query-optimization"; public const string NativeCarouselFlow = "native-carousel-flow"; public const string NativeCreateAccountFlow = "native-create-account-flow"; public const string AccountDeprovisioning = "pm-10308-account-deprovisioning"; From c703390ba2944621f541e4bd737f27a3fe0136be Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 28 Nov 2024 09:49:09 +0100 Subject: [PATCH 21/94] feat: add credential sync feature flag (#5052) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 8077494794..561f5f27c8 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -155,6 +155,7 @@ public static class FeatureFlagKeys public const string SecurityTasks = "security-tasks"; public const string PM14401_ScaleMSPOnClientOrganizationUpdate = "PM-14401-scale-msp-on-client-organization-update"; public const string DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship"; + public const string MacOsNativeCredentialSync = "macos-native-credential-sync"; public static List GetAllKeys() { From 193f8d661216042b2ce423c6d3e5e0a9a1a96eac Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Mon, 2 Dec 2024 06:42:17 -0500 Subject: [PATCH 22/94] Update version to 2024.12.0 (#5099) --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 4e252c82ed..8639ac4a0d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2024.11.0 + 2024.12.0 Bit.$(MSBuildProjectName) enable @@ -64,4 +64,4 @@ - \ No newline at end of file + From ac42b81f7c426db11a8a7fb4632a1fc295cf830c Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Mon, 2 Dec 2024 10:19:21 -0500 Subject: [PATCH 23/94] [PM-14862] Update documentation response type. (#5083) Update documentation to align with the code's response type. --- .../AdminConsole/Public/Controllers/OrganizationController.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Api/AdminConsole/Public/Controllers/OrganizationController.cs b/src/Api/AdminConsole/Public/Controllers/OrganizationController.cs index 9d0b902f89..c1715f471c 100644 --- a/src/Api/AdminConsole/Public/Controllers/OrganizationController.cs +++ b/src/Api/AdminConsole/Public/Controllers/OrganizationController.cs @@ -1,6 +1,5 @@ using System.Net; using Bit.Api.AdminConsole.Public.Models.Request; -using Bit.Api.AdminConsole.Public.Models.Response; using Bit.Api.Models.Public.Response; using Bit.Core.Context; using Bit.Core.Enums; @@ -38,7 +37,7 @@ public class OrganizationController : Controller /// /// The request model. [HttpPost("import")] - [ProducesResponseType(typeof(MemberResponseModel), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(OkResult), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] public async Task Import([FromBody] OrganizationImportRequestModel model) { From 6a77a6d8eebace51a8057b3f643135541cfbf589 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Tue, 3 Dec 2024 09:58:46 -0500 Subject: [PATCH 24/94] [PM-14552] Update error messages copy (#5059) * update error messages * fix tests --- .../Implementations/OrganizationService.cs | 32 ++++++-- .../Services/OrganizationServiceTests.cs | 78 ++++++++++++++++--- 2 files changed, 94 insertions(+), 16 deletions(-) diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 47c79aa13e..e8756fb325 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -2332,10 +2332,13 @@ public class OrganizationService : IOrganizationService PolicyType.SingleOrg, OrganizationUserStatusType.Revoked); var singleOrgPolicyApplies = singleOrgPoliciesApplyingToRevokedUsers.Any(p => p.OrganizationId == orgUser.OrganizationId); + var singleOrgCompliant = true; + var belongsToOtherOrgCompliant = true; + var twoFactorCompliant = true; + if (hasOtherOrgs && singleOrgPolicyApplies) { - throw new BadRequestException("You cannot restore this user until " + - "they leave or remove all other organizations."); + singleOrgCompliant = false; } // Enforce Single Organization Policy of other organizations user is a member of @@ -2343,8 +2346,7 @@ public class OrganizationService : IOrganizationService PolicyType.SingleOrg); if (anySingleOrgPolicies) { - throw new BadRequestException("You cannot restore this user because they are a member of " + - "another organization which forbids it"); + belongsToOtherOrgCompliant = false; } // Enforce Two Factor Authentication Policy of organization user is trying to join @@ -2354,10 +2356,28 @@ public class OrganizationService : IOrganizationService PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) { - throw new BadRequestException("You cannot restore this user until they enable " + - "two-step login on their user account."); + twoFactorCompliant = false; } } + + var user = await _userRepository.GetByIdAsync(userId); + + if (!singleOrgCompliant && !twoFactorCompliant) + { + throw new BadRequestException(user.Email + " is not compliant with the single organization and two-step login polciy"); + } + else if (!singleOrgCompliant) + { + throw new BadRequestException(user.Email + " is not compliant with the single organization policy"); + } + else if (!belongsToOtherOrgCompliant) + { + throw new BadRequestException(user.Email + " belongs to an organization that doesn't allow them to join multiple organizations"); + } + else if (!twoFactorCompliant) + { + throw new BadRequestException(user.Email + " is not compliant with the two-step login policy"); + } } static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser) diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index e09293f32d..f0b7084fe9 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -1833,11 +1833,14 @@ OrganizationUserInvite invite, SutProvider sutProvider) .AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any()) .Returns(true); + var user = new User(); + user.Email = "test@bitwarden.com"; + sutProvider.GetDependency().GetByIdAsync(organizationUser.UserId.Value).Returns(user); + var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); - Assert.Contains("you cannot restore this user because they are a member of " + - "another organization which forbids it", exception.Message.ToLowerInvariant()); + Assert.Contains("test@bitwarden.com belongs to an organization that doesn't allow them to join multiple organizations", exception.Message.ToLowerInvariant()); await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any(), Arg.Any()); await eventService.DidNotReceiveWithAnyArgs() @@ -1865,11 +1868,14 @@ OrganizationUserInvite invite, SutProvider sutProvider) .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any()) .Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } }); + var user = new User(); + user.Email = "test@bitwarden.com"; + sutProvider.GetDependency().GetByIdAsync(organizationUser.UserId.Value).Returns(user); + var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); - Assert.Contains("you cannot restore this user until they enable " + - "two-step login on their user account.", exception.Message.ToLowerInvariant()); + Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant()); await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any(), Arg.Any()); await eventService.DidNotReceiveWithAnyArgs() @@ -1924,11 +1930,14 @@ OrganizationUserInvite invite, SutProvider sutProvider) new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked } }); + var user = new User(); + user.Email = "test@bitwarden.com"; + sutProvider.GetDependency().GetByIdAsync(organizationUser.UserId.Value).Returns(user); + var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); - Assert.Contains("you cannot restore this user until " + - "they leave or remove all other organizations.", exception.Message.ToLowerInvariant()); + Assert.Contains("test@bitwarden.com is not compliant with the single organization policy", exception.Message.ToLowerInvariant()); await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any(), Arg.Any()); await eventService.DidNotReceiveWithAnyArgs() @@ -1958,11 +1967,57 @@ OrganizationUserInvite invite, SutProvider sutProvider) .AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any()) .Returns(true); + var user = new User(); + user.Email = "test@bitwarden.com"; + sutProvider.GetDependency().GetByIdAsync(organizationUser.UserId.Value).Returns(user); + var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); - Assert.Contains("you cannot restore this user because they are a member of " + - "another organization which forbids it", exception.Message.ToLowerInvariant()); + Assert.Contains("test@bitwarden.com belongs to an organization that doesn't allow them to join multiple organizations", exception.Message.ToLowerInvariant()); + + await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any(), Arg.Any()); + await eventService.DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task RestoreUser_WithSingleOrgPolicyEnabled_And_2FA_Policy_Fails( + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser, + SutProvider sutProvider) + { + organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke + secondOrganizationUser.UserId = organizationUser.UserId; + RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); + var organizationUserRepository = sutProvider.GetDependency(); + var eventService = sutProvider.GetDependency(); + + organizationUserRepository.GetManyByUserAsync(organizationUser.UserId.Value).Returns(new[] { organizationUser, secondOrganizationUser }); + sutProvider.GetDependency() + .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any()) + .Returns(new[] + { + new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked } + }); + + sutProvider.GetDependency() + .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any()) + .Returns(new[] + { + new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication, OrganizationUserStatus = OrganizationUserStatusType.Revoked } + }); + + var user = new User(); + user.Email = "test@bitwarden.com"; + sutProvider.GetDependency().GetByIdAsync(organizationUser.UserId.Value).Returns(user); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); + + Assert.Contains("test@bitwarden.com is not compliant with the single organization and two-step login polciy", exception.Message.ToLowerInvariant()); await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any(), Arg.Any()); await eventService.DidNotReceiveWithAnyArgs() @@ -1986,11 +2041,14 @@ OrganizationUserInvite invite, SutProvider sutProvider) .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any()) .Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } }); + var user = new User(); + user.Email = "test@bitwarden.com"; + sutProvider.GetDependency().GetByIdAsync(organizationUser.UserId.Value).Returns(user); + var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); - Assert.Contains("you cannot restore this user until they enable " + - "two-step login on their user account.", exception.Message.ToLowerInvariant()); + Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant()); await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any(), Arg.Any()); await eventService.DidNotReceiveWithAnyArgs() From 059e6816f2b24854663e9d0e7304072019cb80ce Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Tue, 3 Dec 2024 11:01:45 -0600 Subject: [PATCH 25/94] Fixing migration script. (#5093) --- util/Migrator/DbScripts/2024-11-26-00_OrgUserSetStatusBulk.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/Migrator/DbScripts/2024-11-26-00_OrgUserSetStatusBulk.sql b/util/Migrator/DbScripts/2024-11-26-00_OrgUserSetStatusBulk.sql index 95151f90de..5c51f9da40 100644 --- a/util/Migrator/DbScripts/2024-11-26-00_OrgUserSetStatusBulk.sql +++ b/util/Migrator/DbScripts/2024-11-26-00_OrgUserSetStatusBulk.sql @@ -1,4 +1,4 @@ -CREATE PROCEDURE [dbo].[OrganizationUser_SetStatusForUsersById] +CREATE OR ALTER PROCEDURE[dbo].[OrganizationUser_SetStatusForUsersById] @OrganizationUserIds AS NVARCHAR(MAX), @Status SMALLINT AS From b580d7c02210e9d18094f6f713ada52ce387198a Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:06:38 -0500 Subject: [PATCH 26/94] Automatically forwarding ports 1080 and 1433 in VS Code/Cursor (#5104) --- .devcontainer/internal_dev/devcontainer.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.devcontainer/internal_dev/devcontainer.json b/.devcontainer/internal_dev/devcontainer.json index ee9ab7a96d..78a79180ee 100644 --- a/.devcontainer/internal_dev/devcontainer.json +++ b/.devcontainer/internal_dev/devcontainer.json @@ -21,10 +21,15 @@ } }, "postCreateCommand": "bash .devcontainer/internal_dev/postCreateCommand.sh", + "forwardPorts": [1080, 1433], "portsAttributes": { "1080": { "label": "Mail Catcher", "onAutoForward": "notify" + }, + "1433": { + "label": "SQL Server", + "onAutoForward": "notify" } } } From c9aa61b0cf7382630553782d0301c0f9987fffe5 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:38:34 -0500 Subject: [PATCH 27/94] Updated dev container to give the option of installing the Stripe CLI (#5105) --- .devcontainer/internal_dev/postCreateCommand.sh | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.devcontainer/internal_dev/postCreateCommand.sh b/.devcontainer/internal_dev/postCreateCommand.sh index b013be1cec..668b776447 100755 --- a/.devcontainer/internal_dev/postCreateCommand.sh +++ b/.devcontainer/internal_dev/postCreateCommand.sh @@ -70,6 +70,22 @@ Press to continue." sleep 5 # wait for DB container to start dotnet run --project ./util/MsSqlMigratorUtility "$SQL_CONNECTION_STRING" fi + read -r -p "Would you like to install the Stripe CLI? [y/N] " stripe_response + if [[ "$stripe_response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + install_stripe_cli + fi +} + +# Install Stripe CLI +install_stripe_cli() { + echo "Installing Stripe CLI..." + # Add Stripe CLI GPG key so that apt can verify the packages authenticity. + # If Stripe ever changes the key, we'll need to update this. Visit https://docs.stripe.com/stripe-cli?install-method=apt if so + curl -s https://packages.stripe.dev/api/security/keypair/stripe-cli-gpg/public | gpg --dearmor | sudo tee /usr/share/keyrings/stripe.gpg >/dev/null + # Add Stripe CLI repository to apt sources + echo "deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe.dev/stripe-cli-debian-local stable main" | sudo tee -a /etc/apt/sources.list.d/stripe.list >/dev/null + sudo apt update + sudo apt install -y stripe } # main From 44b687922d7756e4771e0572a19af596b447a9df Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:50:47 +1000 Subject: [PATCH 28/94] [PM-14245] Remove policy definitions feature flag (#5095) * Remove PolicyService.SaveAsync and use command instead * Delete feature flag definition * Add public api integration tests --- .../Controllers/PoliciesController.cs | 29 +- .../Models/Request/PolicyRequestModel.cs | 25 +- .../Public/Controllers/PoliciesController.cs | 20 +- .../Request/PolicyUpdateRequestModel.cs | 27 +- .../Models/Response/PolicyResponseModel.cs | 6 +- .../VerifyOrganizationDomainCommand.cs | 21 +- .../Policies/ISavePolicyCommand.cs | 5 +- .../Implementations/SavePolicyCommand.cs | 4 +- .../AdminConsole/Services/IPolicyService.cs | 5 +- .../Services/Implementations/PolicyService.cs | 276 +------ .../Implementations/SsoConfigService.cs | 47 +- src/Core/Constants.cs | 1 - .../Controllers/PoliciesControllerTests.cs | 163 ++++ .../Controllers/PoliciesControllerTests.cs | 33 - .../VerifyOrganizationDomainCommandTests.cs | 28 +- .../Services/PolicyServiceTests.cs | 701 ------------------ .../Auth/Services/SsoConfigServiceTests.cs | 25 +- 17 files changed, 290 insertions(+), 1126 deletions(-) create mode 100644 test/Api.IntegrationTest/AdminConsole/Public/Controllers/PoliciesControllerTests.cs delete mode 100644 test/Api.Test/AdminConsole/Public/Controllers/PoliciesControllerTests.cs diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs index ee48cdd5d4..1167d7a86c 100644 --- a/src/Api/AdminConsole/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs @@ -6,8 +6,8 @@ using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Context; using Bit.Core.Enums; @@ -28,7 +28,6 @@ namespace Bit.Api.AdminConsole.Controllers; public class PoliciesController : Controller { private readonly IPolicyRepository _policyRepository; - private readonly IPolicyService _policyService; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IUserService _userService; private readonly ICurrentContext _currentContext; @@ -37,10 +36,10 @@ public class PoliciesController : Controller private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; private readonly IFeatureService _featureService; private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; + private readonly ISavePolicyCommand _savePolicyCommand; public PoliciesController( IPolicyRepository policyRepository, - IPolicyService policyService, IOrganizationUserRepository organizationUserRepository, IUserService userService, ICurrentContext currentContext, @@ -48,10 +47,10 @@ public class PoliciesController : Controller IDataProtectionProvider dataProtectionProvider, IDataProtectorTokenFactory orgUserInviteTokenDataFactory, IFeatureService featureService, - IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery) + IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery, + ISavePolicyCommand savePolicyCommand) { _policyRepository = policyRepository; - _policyService = policyService; _organizationUserRepository = organizationUserRepository; _userService = userService; _currentContext = currentContext; @@ -62,6 +61,7 @@ public class PoliciesController : Controller _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; _featureService = featureService; _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; + _savePolicyCommand = savePolicyCommand; } [HttpGet("{type}")] @@ -178,25 +178,20 @@ public class PoliciesController : Controller } [HttpPut("{type}")] - public async Task Put(string orgId, int type, [FromBody] PolicyRequestModel model) + public async Task Put(Guid orgId, PolicyType type, [FromBody] PolicyRequestModel model) { - var orgIdGuid = new Guid(orgId); - if (!await _currentContext.ManagePolicies(orgIdGuid)) + if (!await _currentContext.ManagePolicies(orgId)) { throw new NotFoundException(); } - var policy = await _policyRepository.GetByOrganizationIdTypeAsync(new Guid(orgId), (PolicyType)type); - if (policy == null) + + if (type != model.Type) { - policy = model.ToPolicy(orgIdGuid); - } - else - { - policy = model.ToPolicy(policy); + throw new BadRequestException("Mismatched policy type"); } - var userId = _userService.GetProperUserId(User); - await _policyService.SaveAsync(policy, userId); + var policyUpdate = await model.ToPolicyUpdateAsync(orgId, _currentContext); + var policy = await _savePolicyCommand.SaveAsync(policyUpdate); return new PolicyResponseModel(policy); } } diff --git a/src/Api/AdminConsole/Models/Request/PolicyRequestModel.cs b/src/Api/AdminConsole/Models/Request/PolicyRequestModel.cs index db191194d7..a243f46b2e 100644 --- a/src/Api/AdminConsole/Models/Request/PolicyRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/PolicyRequestModel.cs @@ -1,7 +1,9 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json; -using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.Context; namespace Bit.Api.AdminConsole.Models.Request; @@ -13,19 +15,12 @@ public class PolicyRequestModel public bool? Enabled { get; set; } public Dictionary Data { get; set; } - public Policy ToPolicy(Guid orgId) + public async Task ToPolicyUpdateAsync(Guid organizationId, ICurrentContext currentContext) => new() { - return ToPolicy(new Policy - { - Type = Type.Value, - OrganizationId = orgId - }); - } - - public Policy ToPolicy(Policy existingPolicy) - { - existingPolicy.Enabled = Enabled.GetValueOrDefault(); - existingPolicy.Data = Data != null ? JsonSerializer.Serialize(Data) : null; - return existingPolicy; - } + Type = Type!.Value, + OrganizationId = organizationId, + Data = Data != null ? JsonSerializer.Serialize(Data) : null, + Enabled = Enabled.GetValueOrDefault(), + PerformedBy = new StandardUser(currentContext.UserId!.Value, await currentContext.OrganizationOwner(organizationId)) + }; } diff --git a/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs index f2e7c35d24..a22c05ed62 100644 --- a/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs @@ -3,6 +3,7 @@ using Bit.Api.AdminConsole.Public.Models.Request; using Bit.Api.AdminConsole.Public.Models.Response; using Bit.Api.Models.Public.Response; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Context; @@ -18,15 +19,18 @@ public class PoliciesController : Controller private readonly IPolicyRepository _policyRepository; private readonly IPolicyService _policyService; private readonly ICurrentContext _currentContext; + private readonly ISavePolicyCommand _savePolicyCommand; public PoliciesController( IPolicyRepository policyRepository, IPolicyService policyService, - ICurrentContext currentContext) + ICurrentContext currentContext, + ISavePolicyCommand savePolicyCommand) { _policyRepository = policyRepository; _policyService = policyService; _currentContext = currentContext; + _savePolicyCommand = savePolicyCommand; } /// @@ -80,17 +84,9 @@ public class PoliciesController : Controller [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task Put(PolicyType type, [FromBody] PolicyUpdateRequestModel model) { - var policy = await _policyRepository.GetByOrganizationIdTypeAsync( - _currentContext.OrganizationId.Value, type); - if (policy == null) - { - policy = model.ToPolicy(_currentContext.OrganizationId.Value, type); - } - else - { - policy = model.ToPolicy(policy); - } - await _policyService.SaveAsync(policy, null); + var policyUpdate = model.ToPolicyUpdate(_currentContext.OrganizationId!.Value, type); + var policy = await _savePolicyCommand.SaveAsync(policyUpdate); + var response = new PolicyResponseModel(policy); return new JsonResult(response); } diff --git a/src/Api/AdminConsole/Public/Models/Request/PolicyUpdateRequestModel.cs b/src/Api/AdminConsole/Public/Models/Request/PolicyUpdateRequestModel.cs index f859686b81..eb56690462 100644 --- a/src/Api/AdminConsole/Public/Models/Request/PolicyUpdateRequestModel.cs +++ b/src/Api/AdminConsole/Public/Models/Request/PolicyUpdateRequestModel.cs @@ -1,26 +1,19 @@ using System.Text.Json; -using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.Enums; namespace Bit.Api.AdminConsole.Public.Models.Request; public class PolicyUpdateRequestModel : PolicyBaseModel { - public Policy ToPolicy(Guid orgId, PolicyType type) + public PolicyUpdate ToPolicyUpdate(Guid organizationId, PolicyType type) => new() { - return ToPolicy(new Policy - { - OrganizationId = orgId, - Enabled = Enabled.GetValueOrDefault(), - Data = Data != null ? JsonSerializer.Serialize(Data) : null, - Type = type - }); - } - - public virtual Policy ToPolicy(Policy existingPolicy) - { - existingPolicy.Enabled = Enabled.GetValueOrDefault(); - existingPolicy.Data = Data != null ? JsonSerializer.Serialize(Data) : null; - return existingPolicy; - } + Type = type, + OrganizationId = organizationId, + Data = Data != null ? JsonSerializer.Serialize(Data) : null, + Enabled = Enabled.GetValueOrDefault(), + PerformedBy = new SystemUser(EventSystemUser.PublicApi) + }; } diff --git a/src/Api/AdminConsole/Public/Models/Response/PolicyResponseModel.cs b/src/Api/AdminConsole/Public/Models/Response/PolicyResponseModel.cs index 27da5cc561..8da7d93cf1 100644 --- a/src/Api/AdminConsole/Public/Models/Response/PolicyResponseModel.cs +++ b/src/Api/AdminConsole/Public/Models/Response/PolicyResponseModel.cs @@ -1,8 +1,9 @@ using System.ComponentModel.DataAnnotations; -using System.Text.Json; using Bit.Api.Models.Public.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Newtonsoft.Json; +using JsonSerializer = System.Text.Json.JsonSerializer; namespace Bit.Api.AdminConsole.Public.Models.Response; @@ -11,6 +12,9 @@ namespace Bit.Api.AdminConsole.Public.Models.Response; /// public class PolicyResponseModel : PolicyBaseModel, IResponseModel { + [JsonConstructor] + public PolicyResponseModel() { } + public PolicyResponseModel(Policy policy) { if (policy == null) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs index dd6add669f..375c6326ef 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs @@ -1,8 +1,8 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; -using Bit.Core.AdminConsole.Services; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -19,9 +19,9 @@ public class VerifyOrganizationDomainCommand( IDnsResolverService dnsResolverService, IEventService eventService, IGlobalSettings globalSettings, - IPolicyService policyService, IFeatureService featureService, ICurrentContext currentContext, + ISavePolicyCommand savePolicyCommand, ILogger logger) : IVerifyOrganizationDomainCommand { @@ -125,10 +125,15 @@ public class VerifyOrganizationDomainCommand( { if (featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) { - await policyService.SaveAsync( - new Policy { OrganizationId = organizationId, Type = PolicyType.SingleOrg, Enabled = true }, - savingUserId: actingUser is StandardUser standardUser ? standardUser.UserId : null, - eventSystemUser: actingUser is SystemUser systemUser ? systemUser.SystemUserType : null); + var policyUpdate = new PolicyUpdate + { + OrganizationId = organizationId, + Type = PolicyType.SingleOrg, + Enabled = true, + PerformedBy = actingUser + }; + + await savePolicyCommand.SaveAsync(policyUpdate); } } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/ISavePolicyCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/ISavePolicyCommand.cs index 5bfdfc6aa7..6ca842686e 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/ISavePolicyCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/ISavePolicyCommand.cs @@ -1,8 +1,9 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies; public interface ISavePolicyCommand { - Task SaveAsync(PolicyUpdate policy); + Task SaveAsync(PolicyUpdate policy); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs index f193aeabd1..cf332e689a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs @@ -42,7 +42,7 @@ public class SavePolicyCommand : ISavePolicyCommand _policyValidators = policyValidatorsDict; } - public async Task SaveAsync(PolicyUpdate policyUpdate) + public async Task SaveAsync(PolicyUpdate policyUpdate) { var org = await _applicationCacheService.GetOrganizationAbilityAsync(policyUpdate.OrganizationId); if (org == null) @@ -74,6 +74,8 @@ public class SavePolicyCommand : ISavePolicyCommand await _policyRepository.UpsertAsync(policy); await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated); + + return policy; } private async Task RunValidatorAsync(IPolicyValidator validator, PolicyUpdate policyUpdate) diff --git a/src/Core/AdminConsole/Services/IPolicyService.cs b/src/Core/AdminConsole/Services/IPolicyService.cs index 715c3a34d9..4f9a25f904 100644 --- a/src/Core/AdminConsole/Services/IPolicyService.cs +++ b/src/Core/AdminConsole/Services/IPolicyService.cs @@ -1,5 +1,4 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.Entities; using Bit.Core.Enums; @@ -9,8 +8,6 @@ namespace Bit.Core.AdminConsole.Services; public interface IPolicyService { - Task SaveAsync(Policy policy, Guid? savingUserId, EventSystemUser? eventSystemUser = null); - /// /// Get the combined master password policy options for the specified user. /// diff --git a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs index 69ec27fd8a..c3eb2272d0 100644 --- a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs +++ b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs @@ -1,19 +1,8 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Data; +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Repositories; -using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; -using Bit.Core.Context; 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; @@ -24,107 +13,20 @@ namespace Bit.Core.AdminConsole.Services.Implementations; public class PolicyService : IPolicyService { private readonly IApplicationCacheService _applicationCacheService; - private readonly IEventService _eventService; - private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IPolicyRepository _policyRepository; - private readonly ISsoConfigRepository _ssoConfigRepository; - private readonly IMailService _mailService; private readonly GlobalSettings _globalSettings; - private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; - private readonly IFeatureService _featureService; - private readonly ISavePolicyCommand _savePolicyCommand; - private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; - private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; - private readonly ICurrentContext _currentContext; public PolicyService( IApplicationCacheService applicationCacheService, - IEventService eventService, - IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IPolicyRepository policyRepository, - ISsoConfigRepository ssoConfigRepository, - IMailService mailService, - GlobalSettings globalSettings, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IFeatureService featureService, - ISavePolicyCommand savePolicyCommand, - IRemoveOrganizationUserCommand removeOrganizationUserCommand, - IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery, - ICurrentContext currentContext) + GlobalSettings globalSettings) { _applicationCacheService = applicationCacheService; - _eventService = eventService; - _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; _policyRepository = policyRepository; - _ssoConfigRepository = ssoConfigRepository; - _mailService = mailService; _globalSettings = globalSettings; - _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; - _featureService = featureService; - _savePolicyCommand = savePolicyCommand; - _removeOrganizationUserCommand = removeOrganizationUserCommand; - _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; - _currentContext = currentContext; - } - - public async Task SaveAsync(Policy policy, Guid? savingUserId, EventSystemUser? eventSystemUser = null) - { - if (_featureService.IsEnabled(FeatureFlagKeys.Pm13322AddPolicyDefinitions)) - { - // Transitional mapping - this will be moved to callers once the feature flag is removed - // TODO make sure to populate with SystemUser if not an actual user - var policyUpdate = new PolicyUpdate - { - OrganizationId = policy.OrganizationId, - Type = policy.Type, - Enabled = policy.Enabled, - Data = policy.Data, - PerformedBy = savingUserId.HasValue - ? new StandardUser(savingUserId.Value, await _currentContext.OrganizationOwner(policy.OrganizationId)) - : new SystemUser(eventSystemUser ?? EventSystemUser.Unknown) - }; - - await _savePolicyCommand.SaveAsync(policyUpdate); - return; - } - - var org = await _organizationRepository.GetByIdAsync(policy.OrganizationId); - if (org == null) - { - throw new BadRequestException("Organization not found"); - } - - if (!org.UsePolicies) - { - throw new BadRequestException("This organization cannot use policies."); - } - - // FIXME: This method will throw a bunch of errors based on if the - // policy that is being applied requires some other policy that is - // not enabled. It may be advisable to refactor this into a domain - // object and get this kind of stuff out of the service. - await HandleDependentPoliciesAsync(policy, org); - - var now = DateTime.UtcNow; - if (policy.Id == default(Guid)) - { - policy.CreationDate = now; - } - - policy.RevisionDate = now; - - // We can exit early for disable operations, because they are - // simpler. - if (!policy.Enabled) - { - await SetPolicyConfiguration(policy); - return; - } - - await EnablePolicyAsync(policy, org, savingUserId); } public async Task GetMasterPasswordPolicyForUserAsync(User user) @@ -190,178 +92,4 @@ public class PolicyService : IPolicyService return new[] { OrganizationUserType.Owner, OrganizationUserType.Admin }; } - - private async Task DependsOnSingleOrgAsync(Organization org) - { - var singleOrg = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.SingleOrg); - if (singleOrg?.Enabled != true) - { - throw new BadRequestException("Single Organization policy not enabled."); - } - } - - private async Task RequiredBySsoAsync(Organization org) - { - var requireSso = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.RequireSso); - if (requireSso?.Enabled == true) - { - throw new BadRequestException("Single Sign-On Authentication policy is enabled."); - } - } - - private async Task RequiredByKeyConnectorAsync(Organization org) - { - var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id); - if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector) - { - throw new BadRequestException("Key Connector is enabled."); - } - } - - private async Task RequiredByAccountRecoveryAsync(Organization org) - { - var requireSso = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.ResetPassword); - if (requireSso?.Enabled == true) - { - throw new BadRequestException("Account recovery policy is enabled."); - } - } - - private async Task RequiredByVaultTimeoutAsync(Organization org) - { - var vaultTimeout = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.MaximumVaultTimeout); - if (vaultTimeout?.Enabled == true) - { - throw new BadRequestException("Maximum Vault Timeout policy is enabled."); - } - } - - private async Task RequiredBySsoTrustedDeviceEncryptionAsync(Organization org) - { - var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id); - if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption) - { - throw new BadRequestException("Trusted device encryption is on and requires this policy."); - } - } - - private async Task HandleDependentPoliciesAsync(Policy policy, Organization org) - { - switch (policy.Type) - { - case PolicyType.SingleOrg: - if (!policy.Enabled) - { - await HasVerifiedDomainsAsync(org); - await RequiredBySsoAsync(org); - await RequiredByVaultTimeoutAsync(org); - await RequiredByKeyConnectorAsync(org); - await RequiredByAccountRecoveryAsync(org); - } - break; - - case PolicyType.RequireSso: - if (policy.Enabled) - { - await DependsOnSingleOrgAsync(org); - } - else - { - await RequiredByKeyConnectorAsync(org); - await RequiredBySsoTrustedDeviceEncryptionAsync(org); - } - break; - - case PolicyType.ResetPassword: - if (!policy.Enabled || policy.GetDataModel()?.AutoEnrollEnabled == false) - { - await RequiredBySsoTrustedDeviceEncryptionAsync(org); - } - - if (policy.Enabled) - { - await DependsOnSingleOrgAsync(org); - } - break; - - case PolicyType.MaximumVaultTimeout: - if (policy.Enabled) - { - await DependsOnSingleOrgAsync(org); - } - break; - } - } - - private async Task HasVerifiedDomainsAsync(Organization org) - { - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - && await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(org.Id)) - { - throw new BadRequestException("The Single organization policy is required for organizations that have enabled domain verification."); - } - } - - private async Task SetPolicyConfiguration(Policy policy) - { - await _policyRepository.UpsertAsync(policy); - await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated); - } - - private async Task EnablePolicyAsync(Policy policy, Organization org, Guid? savingUserId) - { - var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id); - if (!currentPolicy?.Enabled ?? true) - { - var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(policy.OrganizationId); - var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers); - var removableOrgUsers = orgUsers.Where(ou => - ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked && - ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin && - ou.UserId != savingUserId); - switch (policy.Type) - { - case PolicyType.TwoFactorAuthentication: - // Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled - foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword)) - { - var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == orgUser.Id).twoFactorIsEnabled; - if (!userTwoFactorEnabled) - { - if (!orgUser.HasMasterPassword) - { - throw new BadRequestException( - "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page."); - } - - await _removeOrganizationUserCommand.RemoveUserAsync(policy.OrganizationId, orgUser.Id, - savingUserId); - await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync( - org.DisplayName(), orgUser.Email); - } - } - break; - case PolicyType.SingleOrg: - var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync( - removableOrgUsers.Select(ou => ou.UserId.Value)); - foreach (var orgUser in removableOrgUsers) - { - if (userOrgs.Any(ou => ou.UserId == orgUser.UserId - && ou.OrganizationId != org.Id - && ou.Status != OrganizationUserStatusType.Invited)) - { - await _removeOrganizationUserCommand.RemoveUserAsync(policy.OrganizationId, orgUser.Id, - savingUserId); - await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync( - org.DisplayName(), orgUser.Email); - } - } - break; - default: - break; - } - } - - await SetPolicyConfiguration(policy); - } } diff --git a/src/Core/Auth/Services/Implementations/SsoConfigService.cs b/src/Core/Auth/Services/Implementations/SsoConfigService.cs index 532f000394..bf7e2d56fe 100644 --- a/src/Core/Auth/Services/Implementations/SsoConfigService.cs +++ b/src/Core/Auth/Services/Implementations/SsoConfigService.cs @@ -1,8 +1,9 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; @@ -17,25 +18,25 @@ public class SsoConfigService : ISsoConfigService { private readonly ISsoConfigRepository _ssoConfigRepository; private readonly IPolicyRepository _policyRepository; - private readonly IPolicyService _policyService; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IEventService _eventService; + private readonly ISavePolicyCommand _savePolicyCommand; public SsoConfigService( ISsoConfigRepository ssoConfigRepository, IPolicyRepository policyRepository, - IPolicyService policyService, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, - IEventService eventService) + IEventService eventService, + ISavePolicyCommand savePolicyCommand) { _ssoConfigRepository = ssoConfigRepository; _policyRepository = policyRepository; - _policyService = policyService; _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; _eventService = eventService; + _savePolicyCommand = savePolicyCommand; } public async Task SaveAsync(SsoConfig config, Organization organization) @@ -63,25 +64,29 @@ public class SsoConfigService : ISsoConfigService // Automatically enable account recovery, SSO required, and single org policies if trusted device encryption is selected if (config.GetData().MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption) { - var singleOrgPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.SingleOrg) ?? - new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.SingleOrg }; - singleOrgPolicy.Enabled = true; + await _savePolicyCommand.SaveAsync(new() + { + OrganizationId = config.OrganizationId, + Type = PolicyType.SingleOrg, + Enabled = true + }); - await _policyService.SaveAsync(singleOrgPolicy, null); + var resetPasswordPolicy = new PolicyUpdate + { + OrganizationId = config.OrganizationId, + Type = PolicyType.ResetPassword, + Enabled = true, + }; + resetPasswordPolicy.SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true }); + await _savePolicyCommand.SaveAsync(resetPasswordPolicy); - var resetPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.ResetPassword) ?? - new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.ResetPassword, }; - - resetPolicy.Enabled = true; - resetPolicy.SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true }); - await _policyService.SaveAsync(resetPolicy, null); - - var ssoRequiredPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.RequireSso) ?? - new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.RequireSso, }; - - ssoRequiredPolicy.Enabled = true; - await _policyService.SaveAsync(ssoRequiredPolicy, null); + await _savePolicyCommand.SaveAsync(new() + { + OrganizationId = config.OrganizationId, + Type = PolicyType.RequireSso, + Enabled = true + }); } await LogEventsAsync(config, oldConfig); diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 561f5f27c8..6efdedfeed 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -144,7 +144,6 @@ public static class FeatureFlagKeys public const string AccessIntelligence = "pm-13227-access-intelligence"; public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises"; - public const string Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions"; public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split"; public const string GeneratorToolsModernization = "generator-tools-modernization"; public const string NewDeviceVerification = "new-device-verification"; diff --git a/test/Api.IntegrationTest/AdminConsole/Public/Controllers/PoliciesControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Public/Controllers/PoliciesControllerTests.cs new file mode 100644 index 0000000000..f034426f98 --- /dev/null +++ b/test/Api.IntegrationTest/AdminConsole/Public/Controllers/PoliciesControllerTests.cs @@ -0,0 +1,163 @@ +using System.Net; +using System.Text.Json; +using Bit.Api.AdminConsole.Public.Models.Request; +using Bit.Api.AdminConsole.Public.Models.Response; +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.Helpers; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Enums; +using Bit.Core.Enums; +using Bit.Test.Common.Helpers; +using Xunit; + +namespace Bit.Api.IntegrationTest.AdminConsole.Public.Controllers; + +public class PoliciesControllerTests : IClassFixture, IAsyncLifetime +{ + private readonly HttpClient _client; + private readonly ApiApplicationFactory _factory; + private readonly LoginHelper _loginHelper; + + // These will get set in `InitializeAsync` which is ran before all tests + private Organization _organization = null!; + private string _ownerEmail = null!; + + public PoliciesControllerTests(ApiApplicationFactory factory) + { + _factory = factory; + _client = factory.CreateClient(); + _loginHelper = new LoginHelper(_factory, _client); + } + + public async Task InitializeAsync() + { + // Create the owner account + _ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(_ownerEmail); + + // Create the organization + (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023, + ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card); + + // Authorize with the organization api key + await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id); + } + + public Task DisposeAsync() + { + _client.Dispose(); + return Task.CompletedTask; + } + + [Fact] + public async Task Post_NewPolicy() + { + var policyType = PolicyType.MasterPassword; + var request = new PolicyUpdateRequestModel + { + Enabled = true, + Data = new Dictionary + { + { "minComplexity", 15}, + { "requireLower", true} + } + }; + + var response = await _client.PutAsync($"/public/policies/{policyType}", JsonContent.Create(request)); + + // Assert against the response + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var result = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(result); + + Assert.True(result.Enabled); + Assert.Equal(policyType, result.Type); + Assert.IsType(result.Id); + Assert.NotEqual(default, result.Id); + Assert.NotNull(result.Data); + Assert.Equal(15, ((JsonElement)result.Data["minComplexity"]).GetInt32()); + Assert.True(((JsonElement)result.Data["requireLower"]).GetBoolean()); + + // Assert against the database values + var policyRepository = _factory.GetService(); + var policy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType); + Assert.NotNull(policy); + + Assert.True(policy.Enabled); + Assert.Equal(policyType, policy.Type); + Assert.IsType(policy.Id); + Assert.NotEqual(default, policy.Id); + Assert.Equal(_organization.Id, policy.OrganizationId); + + Assert.NotNull(policy.Data); + var data = policy.GetDataModel(); + var expectedData = new MasterPasswordPolicyData { MinComplexity = 15, RequireLower = true }; + AssertHelper.AssertPropertyEqual(expectedData, data); + } + + [Fact] + public async Task Post_UpdatePolicy() + { + var policyType = PolicyType.MasterPassword; + var existingPolicy = new Policy + { + OrganizationId = _organization.Id, + Enabled = true, + Type = policyType + }; + existingPolicy.SetDataModel(new MasterPasswordPolicyData + { + EnforceOnLogin = true, + MinLength = 22, + RequireSpecial = true + }); + + var policyRepository = _factory.GetService(); + await policyRepository.UpsertAsync(existingPolicy); + + // The Id isn't set until it's created in the database, get it back out to get the id + var createdPolicy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType); + var expectedId = createdPolicy!.Id; + + var request = new PolicyUpdateRequestModel + { + Enabled = false, + Data = new Dictionary + { + { "minLength", 15}, + { "requireUpper", true} + } + }; + + var response = await _client.PutAsync($"/public/policies/{policyType}", JsonContent.Create(request)); + + // Assert against the response + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var result = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(result); + + Assert.False(result.Enabled); + Assert.Equal(policyType, result.Type); + Assert.Equal(expectedId, result.Id); + Assert.NotNull(result.Data); + Assert.Equal(15, ((JsonElement)result.Data["minLength"]).GetInt32()); + Assert.True(((JsonElement)result.Data["requireUpper"]).GetBoolean()); + + // Assert against the database values + var policy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType); + Assert.NotNull(policy); + + Assert.False(policy.Enabled); + Assert.Equal(policyType, policy.Type); + Assert.Equal(expectedId, policy.Id); + Assert.Equal(_organization.Id, policy.OrganizationId); + + Assert.NotNull(policy.Data); + var data = policy.GetDataModel(); + Assert.Equal(15, data.MinLength); + Assert.Equal(true, data.RequireUpper); + } +} diff --git a/test/Api.Test/AdminConsole/Public/Controllers/PoliciesControllerTests.cs b/test/Api.Test/AdminConsole/Public/Controllers/PoliciesControllerTests.cs deleted file mode 100644 index 71d04cae33..0000000000 --- a/test/Api.Test/AdminConsole/Public/Controllers/PoliciesControllerTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Bit.Api.AdminConsole.Public.Controllers; -using Bit.Api.AdminConsole.Public.Models.Request; -using Bit.Api.AdminConsole.Public.Models.Response; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Context; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using Microsoft.AspNetCore.Mvc; -using NSubstitute; -using Xunit; - -namespace Bit.Api.Test.AdminConsole.Public.Controllers; - -[ControllerCustomize(typeof(PoliciesController))] -[SutProviderCustomize] -public class PoliciesControllerTests -{ - [Theory] - [BitAutoData] - [BitAutoData(PolicyType.SendOptions)] - public async Task Put_NewPolicy_AppliesCorrectType(PolicyType type, Organization organization, PolicyUpdateRequestModel model, SutProvider sutProvider) - { - sutProvider.GetDependency().OrganizationId.Returns(organization.Id); - sutProvider.GetDependency().GetByOrganizationIdTypeAsync(organization.Id, type).Returns((Policy)null); - - var response = await sutProvider.Sut.Put(type, model) as JsonResult; - var responseValue = response.Value as PolicyResponseModel; - - Assert.Equal(type, responseValue.Type); - } -} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs index 8dbe533131..700df88d54 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs @@ -1,7 +1,8 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; -using Bit.Core.AdminConsole.Services; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -182,9 +183,13 @@ public class VerifyOrganizationDomainCommandTests _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); - await sutProvider.GetDependency() + await sutProvider.GetDependency() .Received(1) - .SaveAsync(Arg.Is(x => x.Type == PolicyType.SingleOrg && x.OrganizationId == domain.OrganizationId && x.Enabled), userId); + .SaveAsync(Arg.Is(x => x.Type == PolicyType.SingleOrg && + x.OrganizationId == domain.OrganizationId && + x.Enabled && + x.PerformedBy is StandardUser && + x.PerformedBy.UserId == userId)); } [Theory, BitAutoData] @@ -208,9 +213,9 @@ public class VerifyOrganizationDomainCommandTests _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); - await sutProvider.GetDependency() + await sutProvider.GetDependency() .DidNotReceive() - .SaveAsync(Arg.Any(), null); + .SaveAsync(Arg.Any()); } [Theory, BitAutoData] @@ -234,10 +239,9 @@ public class VerifyOrganizationDomainCommandTests _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); - await sutProvider.GetDependency() + await sutProvider.GetDependency() .DidNotReceive() - .SaveAsync(Arg.Any(), null); - + .SaveAsync(Arg.Any()); } [Theory, BitAutoData] @@ -261,8 +265,8 @@ public class VerifyOrganizationDomainCommandTests _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); - await sutProvider.GetDependency() + await sutProvider.GetDependency() .DidNotReceive() - .SaveAsync(Arg.Any(), null); + .SaveAsync(Arg.Any()); } } diff --git a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs index 68f36e37ce..62ab584c4b 100644 --- a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs @@ -1,25 +1,13 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; -using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services.Implementations; -using Bit.Core.Auth.Entities; -using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Models.Data; -using Bit.Core.Auth.Repositories; -using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; 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.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; -using AdminConsoleFixtures = Bit.Core.Test.AdminConsole.AutoFixture; using GlobalSettings = Bit.Core.Settings.GlobalSettings; namespace Bit.Core.Test.AdminConsole.Services; @@ -27,667 +15,6 @@ namespace Bit.Core.Test.AdminConsole.Services; [SutProviderCustomize] public class PolicyServiceTests { - [Theory, BitAutoData] - public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest( - [AdminConsoleFixtures.Policy(PolicyType.DisableSend)] Policy policy, SutProvider sutProvider) - { - SetupOrg(sutProvider, policy.OrganizationId, null); - - var badRequestException = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(policy, - Guid.NewGuid())); - - Assert.Contains("Organization not found", badRequestException.Message, StringComparison.OrdinalIgnoreCase); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .LogPolicyEventAsync(default, default, default); - } - - [Theory, BitAutoData] - public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest( - [AdminConsoleFixtures.Policy(PolicyType.DisableSend)] Policy policy, SutProvider sutProvider) - { - var orgId = Guid.NewGuid(); - - SetupOrg(sutProvider, policy.OrganizationId, new Organization - { - UsePolicies = false, - }); - - var badRequestException = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(policy, - Guid.NewGuid())); - - Assert.Contains("cannot use policies", badRequestException.Message, StringComparison.OrdinalIgnoreCase); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .LogPolicyEventAsync(default, default, default); - } - - [Theory, BitAutoData] - public async Task SaveAsync_SingleOrg_RequireSsoEnabled_ThrowsBadRequest( - [AdminConsoleFixtures.Policy(PolicyType.SingleOrg)] Policy policy, SutProvider sutProvider) - { - policy.Enabled = false; - - SetupOrg(sutProvider, policy.OrganizationId, new Organization - { - Id = policy.OrganizationId, - UsePolicies = true, - }); - - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.RequireSso) - .Returns(Task.FromResult(new Policy { Enabled = true })); - - var badRequestException = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(policy, - Guid.NewGuid())); - - Assert.Contains("Single Sign-On Authentication policy is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .LogPolicyEventAsync(default, default, default); - } - - [Theory, BitAutoData] - public async Task SaveAsync_SingleOrg_VaultTimeoutEnabled_ThrowsBadRequest([AdminConsoleFixtures.Policy(PolicyType.SingleOrg)] Policy policy, SutProvider sutProvider) - { - policy.Enabled = false; - - SetupOrg(sutProvider, policy.OrganizationId, new Organization - { - Id = policy.OrganizationId, - UsePolicies = true, - }); - - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.MaximumVaultTimeout) - .Returns(new Policy { Enabled = true }); - - var badRequestException = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(policy, - Guid.NewGuid())); - - Assert.Contains("Maximum Vault Timeout policy is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - } - - [Theory] - [BitAutoData(PolicyType.SingleOrg)] - [BitAutoData(PolicyType.RequireSso)] - public async Task SaveAsync_PolicyRequiredByKeyConnector_DisablePolicy_ThrowsBadRequest( - PolicyType policyType, - Policy policy, - SutProvider sutProvider) - { - policy.Enabled = false; - policy.Type = policyType; - - SetupOrg(sutProvider, policy.OrganizationId, new Organization - { - Id = policy.OrganizationId, - UsePolicies = true, - }); - - var ssoConfig = new SsoConfig { Enabled = true }; - var data = new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector }; - ssoConfig.SetData(data); - - sutProvider.GetDependency() - .GetByOrganizationIdAsync(policy.OrganizationId) - .Returns(ssoConfig); - - var badRequestException = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(policy, - Guid.NewGuid())); - - Assert.Contains("Key Connector is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - } - - [Theory, BitAutoData] - public async Task SaveAsync_RequireSsoPolicy_NotEnabled_ThrowsBadRequestAsync( - [AdminConsoleFixtures.Policy(PolicyType.RequireSso)] Policy policy, SutProvider sutProvider) - { - policy.Enabled = true; - - SetupOrg(sutProvider, policy.OrganizationId, new Organization - { - Id = policy.OrganizationId, - UsePolicies = true, - }); - - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.SingleOrg) - .Returns(Task.FromResult(new Policy { Enabled = false })); - - var badRequestException = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(policy, - Guid.NewGuid())); - - Assert.Contains("Single Organization policy not enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .LogPolicyEventAsync(default, default, default); - } - - [Theory, BitAutoData] - public async Task SaveAsync_NewPolicy_Created( - [AdminConsoleFixtures.Policy(PolicyType.ResetPassword)] Policy policy, SutProvider sutProvider) - { - policy.Id = default; - policy.Data = null; - - SetupOrg(sutProvider, policy.OrganizationId, new Organization - { - Id = policy.OrganizationId, - UsePolicies = true, - }); - - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.SingleOrg) - .Returns(Task.FromResult(new Policy { Enabled = true })); - - var utcNow = DateTime.UtcNow; - - await sutProvider.Sut.SaveAsync(policy, Guid.NewGuid()); - - await sutProvider.GetDependency().Received() - .LogPolicyEventAsync(policy, EventType.Policy_Updated); - - await sutProvider.GetDependency().Received() - .UpsertAsync(policy); - - Assert.True(policy.CreationDate - utcNow < TimeSpan.FromSeconds(1)); - Assert.True(policy.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); - } - - [Theory, BitAutoData] - public async Task SaveAsync_VaultTimeoutPolicy_NotEnabled_ThrowsBadRequestAsync( - [AdminConsoleFixtures.Policy(PolicyType.MaximumVaultTimeout)] Policy policy, SutProvider sutProvider) - { - policy.Enabled = true; - - SetupOrg(sutProvider, policy.OrganizationId, new Organization - { - Id = policy.OrganizationId, - UsePolicies = true, - }); - - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.SingleOrg) - .Returns(Task.FromResult(new Policy { Enabled = false })); - - var badRequestException = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(policy, - Guid.NewGuid())); - - Assert.Contains("Single Organization policy not enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .LogPolicyEventAsync(default, default, default); - } - - [Theory, BitAutoData] - public async Task SaveAsync_ExistingPolicy_UpdateTwoFactor( - Organization organization, - [AdminConsoleFixtures.Policy(PolicyType.TwoFactorAuthentication)] Policy policy, - SutProvider sutProvider) - { - // If the policy that this is updating isn't enabled then do some work now that the current one is enabled - - organization.UsePolicies = true; - policy.OrganizationId = organization.Id; - - SetupOrg(sutProvider, organization.Id, organization); - - sutProvider.GetDependency() - .GetByIdAsync(policy.Id) - .Returns(new Policy - { - Id = policy.Id, - Type = PolicyType.TwoFactorAuthentication, - Enabled = false - }); - - var orgUserDetailUserInvited = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Invited, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user1@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = false - }; - var orgUserDetailUserAcceptedWith2FA = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Accepted, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user2@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = true - }; - var orgUserDetailUserAcceptedWithout2FA = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Accepted, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user3@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = true - }; - var orgUserDetailAdmin = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.Admin, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "admin@test.com", - Name = "ADMIN", - UserId = Guid.NewGuid(), - HasMasterPassword = false - }; - - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policy.OrganizationId) - .Returns(new List - { - orgUserDetailUserInvited, - orgUserDetailUserAcceptedWith2FA, - orgUserDetailUserAcceptedWithout2FA, - orgUserDetailAdmin - }); - - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(Arg.Any>()) - .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>() - { - (orgUserDetailUserInvited, false), - (orgUserDetailUserAcceptedWith2FA, true), - (orgUserDetailUserAcceptedWithout2FA, false), - (orgUserDetailAdmin, false), - }); - - var removeOrganizationUserCommand = sutProvider.GetDependency(); - - var utcNow = DateTime.UtcNow; - - var savingUserId = Guid.NewGuid(); - - await sutProvider.Sut.SaveAsync(policy, savingUserId); - - await removeOrganizationUserCommand.Received() - .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWithout2FA.Id, savingUserId); - await sutProvider.GetDependency().Received() - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWithout2FA.Email); - - await removeOrganizationUserCommand.DidNotReceive() - .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserInvited.Id, savingUserId); - await sutProvider.GetDependency().DidNotReceive() - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserInvited.Email); - await removeOrganizationUserCommand.DidNotReceive() - .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWith2FA.Id, savingUserId); - await sutProvider.GetDependency().DidNotReceive() - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWith2FA.Email); - await removeOrganizationUserCommand.DidNotReceive() - .RemoveUserAsync(policy.OrganizationId, orgUserDetailAdmin.Id, savingUserId); - await sutProvider.GetDependency().DidNotReceive() - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailAdmin.Email); - - await sutProvider.GetDependency().Received() - .LogPolicyEventAsync(policy, EventType.Policy_Updated); - - await sutProvider.GetDependency().Received() - .UpsertAsync(policy); - - Assert.True(policy.CreationDate - utcNow < TimeSpan.FromSeconds(1)); - Assert.True(policy.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); - } - - [Theory, BitAutoData] - public async Task SaveAsync_EnableTwoFactor_WithoutMasterPasswordOr2FA_ThrowsBadRequest( - Organization organization, - [AdminConsoleFixtures.Policy(PolicyType.TwoFactorAuthentication)] Policy policy, - SutProvider sutProvider) - { - organization.UsePolicies = true; - policy.OrganizationId = organization.Id; - - SetupOrg(sutProvider, organization.Id, organization); - - var orgUserDetailUserWith2FAAndMP = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user1@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = true - }; - var orgUserDetailUserWith2FANoMP = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user2@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = false - }; - var orgUserDetailUserWithout2FA = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user3@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = false - }; - var orgUserDetailAdmin = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.Admin, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "admin@test.com", - Name = "ADMIN", - UserId = Guid.NewGuid(), - HasMasterPassword = false - }; - - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policy.OrganizationId) - .Returns(new List - { - orgUserDetailUserWith2FAAndMP, - orgUserDetailUserWith2FANoMP, - orgUserDetailUserWithout2FA, - orgUserDetailAdmin - }); - - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(Arg.Is>(ids => - ids.Contains(orgUserDetailUserWith2FANoMP.UserId.Value) - && ids.Contains(orgUserDetailUserWithout2FA.UserId.Value) - && ids.Contains(orgUserDetailAdmin.UserId.Value))) - .Returns(new List<(Guid userId, bool hasTwoFactor)>() - { - (orgUserDetailUserWith2FANoMP.UserId.Value, true), - (orgUserDetailUserWithout2FA.UserId.Value, false), - (orgUserDetailAdmin.UserId.Value, false), - }); - - var removeOrganizationUserCommand = sutProvider.GetDependency(); - - var savingUserId = Guid.NewGuid(); - - var badRequestException = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(policy, savingUserId)); - - Assert.Contains("Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); - - await removeOrganizationUserCommand.DidNotReceiveWithAnyArgs() - .RemoveUserAsync(organizationId: default, organizationUserId: default, deletingUserId: default); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(default, default); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .LogPolicyEventAsync(default, default); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - } - - [Theory, BitAutoData] - public async Task SaveAsync_ExistingPolicy_UpdateSingleOrg( - [AdminConsoleFixtures.Policy(PolicyType.TwoFactorAuthentication)] Policy policy, SutProvider sutProvider) - { - // If the policy that this is updating isn't enabled then do some work now that the current one is enabled - - var org = new Organization - { - Id = policy.OrganizationId, - UsePolicies = true, - Name = "TEST", - }; - - SetupOrg(sutProvider, policy.OrganizationId, org); - - sutProvider.GetDependency() - .GetByIdAsync(policy.Id) - .Returns(new Policy - { - Id = policy.Id, - Type = PolicyType.SingleOrg, - Enabled = false, - }); - - var orgUserDetail = new Core.Models.Data.Organizations.OrganizationUsers.OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Accepted, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "test@bitwarden.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = true - }; - - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policy.OrganizationId) - .Returns(new List - { - orgUserDetail, - }); - - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(orgUserDetail.UserId.Value))) - .Returns(new List<(Guid userId, bool hasTwoFactor)>() - { - (orgUserDetail.UserId.Value, false), - }); - - var utcNow = DateTime.UtcNow; - - var savingUserId = Guid.NewGuid(); - - await sutProvider.Sut.SaveAsync(policy, savingUserId); - - await sutProvider.GetDependency().Received() - .LogPolicyEventAsync(policy, EventType.Policy_Updated); - - await sutProvider.GetDependency().Received() - .UpsertAsync(policy); - - Assert.True(policy.CreationDate - utcNow < TimeSpan.FromSeconds(1)); - Assert.True(policy.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); - } - - [Theory] - [BitAutoData(true, false)] - [BitAutoData(false, true)] - [BitAutoData(false, false)] - public async Task SaveAsync_ResetPasswordPolicyRequiredByTrustedDeviceEncryption_DisablePolicyOrDisableAutomaticEnrollment_ThrowsBadRequest( - bool policyEnabled, - bool autoEnrollEnabled, - [AdminConsoleFixtures.Policy(PolicyType.ResetPassword)] Policy policy, - SutProvider sutProvider) - { - policy.Enabled = policyEnabled; - policy.SetDataModel(new ResetPasswordDataModel - { - AutoEnrollEnabled = autoEnrollEnabled - }); - - SetupOrg(sutProvider, policy.OrganizationId, new Organization - { - Id = policy.OrganizationId, - UsePolicies = true, - }); - - var ssoConfig = new SsoConfig { Enabled = true }; - ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption }); - - sutProvider.GetDependency() - .GetByOrganizationIdAsync(policy.OrganizationId) - .Returns(ssoConfig); - - var badRequestException = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(policy, - Guid.NewGuid())); - - Assert.Contains("Trusted device encryption is on and requires this policy.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .LogPolicyEventAsync(default, default, default); - } - - [Theory, BitAutoData] - public async Task SaveAsync_RequireSsoPolicyRequiredByTrustedDeviceEncryption_DisablePolicy_ThrowsBadRequest( - [AdminConsoleFixtures.Policy(PolicyType.RequireSso)] Policy policy, - SutProvider sutProvider) - { - policy.Enabled = false; - - SetupOrg(sutProvider, policy.OrganizationId, new Organization - { - Id = policy.OrganizationId, - UsePolicies = true, - }); - - var ssoConfig = new SsoConfig { Enabled = true }; - ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption }); - - sutProvider.GetDependency() - .GetByOrganizationIdAsync(policy.OrganizationId) - .Returns(ssoConfig); - - var badRequestException = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(policy, - Guid.NewGuid())); - - Assert.Contains("Trusted device encryption is on and requires this policy.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .LogPolicyEventAsync(default, default, default); - } - - [Theory, BitAutoData] - public async Task SaveAsync_PolicyRequiredForAccountRecovery_NotEnabled_ThrowsBadRequestAsync( - [AdminConsoleFixtures.Policy(PolicyType.ResetPassword)] Policy policy, SutProvider sutProvider) - { - policy.Enabled = true; - policy.SetDataModel(new ResetPasswordDataModel()); - - SetupOrg(sutProvider, policy.OrganizationId, new Organization - { - Id = policy.OrganizationId, - UsePolicies = true, - }); - - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.SingleOrg) - .Returns(Task.FromResult(new Policy { Enabled = false })); - - var badRequestException = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(policy, - Guid.NewGuid())); - - Assert.Contains("Single Organization policy not enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .LogPolicyEventAsync(default, default, default); - } - - - [Theory, BitAutoData] - public async Task SaveAsync_SingleOrg_AccountRecoveryEnabled_ThrowsBadRequest( - [AdminConsoleFixtures.Policy(PolicyType.SingleOrg)] Policy policy, SutProvider sutProvider) - { - policy.Enabled = false; - - SetupOrg(sutProvider, policy.OrganizationId, new Organization - { - Id = policy.OrganizationId, - UsePolicies = true, - }); - - sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.ResetPassword) - .Returns(new Policy { Enabled = true }); - - var badRequestException = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(policy, - Guid.NewGuid())); - - Assert.Contains("Account recovery policy is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - } - [Theory, BitAutoData] public async Task GetPoliciesApplicableToUserAsync_WithRequireSsoTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsNoPolicies(Guid userId, SutProvider sutProvider) { @@ -816,32 +143,4 @@ public class PolicyServiceTests new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.DisableSend, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Invited, IsProvider = true } }); } - - - [Theory, BitAutoData] - public async Task SaveAsync_GivenOrganizationUsingPoliciesAndHasVerifiedDomains_WhenSingleOrgPolicyIsDisabled_ThenAnErrorShouldBeThrownOrganizationHasVerifiedDomains( - [AdminConsoleFixtures.Policy(PolicyType.SingleOrg)] Policy policy, Organization org, SutProvider sutProvider) - { - org.Id = policy.OrganizationId; - org.UsePolicies = true; - - policy.Enabled = false; - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - - sutProvider.GetDependency() - .GetByIdAsync(policy.OrganizationId) - .Returns(org); - - sutProvider.GetDependency() - .HasVerifiedDomainsAsync(org.Id) - .Returns(true); - - var badRequestException = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(policy, null)); - - Assert.Equal("The Single organization policy is required for organizations that have enabled domain verification.", badRequestException.Message); - } } diff --git a/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs b/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs index e397c838c6..7beb772b95 100644 --- a/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs +++ b/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs @@ -1,8 +1,9 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; @@ -338,16 +339,26 @@ public class SsoConfigServiceTests await sutProvider.Sut.SaveAsync(ssoConfig, organization); - await sutProvider.GetDependency().Received(1) + await sutProvider.GetDependency().Received(1) .SaveAsync( - Arg.Is(t => t.Type == PolicyType.SingleOrg), - null + Arg.Is(t => t.Type == PolicyType.SingleOrg && + t.OrganizationId == organization.Id && + t.Enabled) ); - await sutProvider.GetDependency().Received(1) + await sutProvider.GetDependency().Received(1) .SaveAsync( - Arg.Is(t => t.Type == PolicyType.ResetPassword && t.GetDataModel().AutoEnrollEnabled), - null + Arg.Is(t => t.Type == PolicyType.ResetPassword && + t.GetDataModel().AutoEnrollEnabled && + t.OrganizationId == organization.Id && + t.Enabled) + ); + + await sutProvider.GetDependency().Received(1) + .SaveAsync( + Arg.Is(t => t.Type == PolicyType.RequireSso && + t.OrganizationId == organization.Id && + t.Enabled) ); await sutProvider.GetDependency().ReceivedWithAnyArgs() From 94fdfa40e8af9c9b788aafe2cf89eacc2913eeea Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Wed, 4 Dec 2024 11:45:11 +0100 Subject: [PATCH 29/94] [PM-13999] Show estimated tax for taxable countries (#5077) --- .../Billing/TaxServiceTests.cs | 151 +++ .../Controllers/AccountsBillingController.cs | 15 + .../Billing/Controllers/InvoicesController.cs | 42 + .../Controllers/ProviderBillingController.cs | 1 + .../Billing/Controllers/StripeController.cs | 14 +- .../Requests/TaxInformationRequestBody.cs | 2 + .../Billing/Extensions/CurrencyExtensions.cs | 33 + .../Extensions/ServiceCollectionExtensions.cs | 1 + .../PreviewIndividualInvoiceRequestModel.cs | 18 + .../PreviewOrganizationInvoiceRequestModel.cs | 37 + .../Requests/TaxInformationRequestModel.cs | 14 + .../Responses/PreviewInvoiceResponseModel.cs | 7 + src/Core/Billing/Models/PreviewInvoiceInfo.cs | 7 + .../Billing/Models/Sales/OrganizationSale.cs | 1 + src/Core/Billing/Models/TaxIdType.cs | 22 + src/Core/Billing/Models/TaxInformation.cs | 160 +--- src/Core/Billing/Services/ITaxService.cs | 22 + .../OrganizationBillingService.cs | 35 +- .../PremiumUserBillingService.cs | 37 +- .../Implementations/SubscriberService.cs | 51 +- src/Core/Billing/Services/TaxService.cs | 901 ++++++++++++++++++ src/Core/Billing/Utilities.cs | 1 + src/Core/Models/Business/TaxInfo.cs | 208 +--- src/Core/Services/IPaymentService.cs | 6 + src/Core/Services/IStripeAdapter.cs | 2 + .../Services/Implementations/StripeAdapter.cs | 12 + .../Implementations/StripePaymentService.cs | 389 +++++++- .../Services/SubscriberServiceTests.cs | 3 +- .../Core.Test/Models/Business/TaxInfoTests.cs | 114 --- .../Services/StripePaymentServiceTests.cs | 18 +- 30 files changed, 1793 insertions(+), 531 deletions(-) create mode 100644 bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs create mode 100644 src/Api/Billing/Controllers/InvoicesController.cs create mode 100644 src/Core/Billing/Extensions/CurrencyExtensions.cs create mode 100644 src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs create mode 100644 src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs create mode 100644 src/Core/Billing/Models/Api/Requests/TaxInformationRequestModel.cs create mode 100644 src/Core/Billing/Models/Api/Responses/PreviewInvoiceResponseModel.cs create mode 100644 src/Core/Billing/Models/PreviewInvoiceInfo.cs create mode 100644 src/Core/Billing/Models/TaxIdType.cs create mode 100644 src/Core/Billing/Services/ITaxService.cs create mode 100644 src/Core/Billing/Services/TaxService.cs delete mode 100644 test/Core.Test/Models/Business/TaxInfoTests.cs diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs new file mode 100644 index 0000000000..3995fb9de6 --- /dev/null +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs @@ -0,0 +1,151 @@ +using Bit.Core.Billing.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Commercial.Core.Test.Billing; + +[SutProviderCustomize] +public class TaxServiceTests +{ + [Theory] + [BitAutoData("AD", "A-123456-Z", "ad_nrt")] + [BitAutoData("AD", "A123456Z", "ad_nrt")] + [BitAutoData("AR", "20-12345678-9", "ar_cuit")] + [BitAutoData("AR", "20123456789", "ar_cuit")] + [BitAutoData("AU", "01259983598", "au_abn")] + [BitAutoData("AU", "123456789123", "au_arn")] + [BitAutoData("AT", "ATU12345678", "eu_vat")] + [BitAutoData("BH", "123456789012345", "bh_vat")] + [BitAutoData("BY", "123456789", "by_tin")] + [BitAutoData("BE", "BE0123456789", "eu_vat")] + [BitAutoData("BO", "123456789", "bo_tin")] + [BitAutoData("BR", "01.234.456/5432-10", "br_cnpj")] + [BitAutoData("BR", "01234456543210", "br_cnpj")] + [BitAutoData("BR", "123.456.789-87", "br_cpf")] + [BitAutoData("BR", "12345678987", "br_cpf")] + [BitAutoData("BG", "123456789", "bg_uic")] + [BitAutoData("BG", "BG012100705", "eu_vat")] + [BitAutoData("CA", "100728494", "ca_bn")] + [BitAutoData("CA", "123456789RT0001", "ca_gst_hst")] + [BitAutoData("CA", "PST-1234-1234", "ca_pst_bc")] + [BitAutoData("CA", "123456-7", "ca_pst_mb")] + [BitAutoData("CA", "1234567", "ca_pst_sk")] + [BitAutoData("CA", "1234567890TQ1234", "ca_qst")] + [BitAutoData("CL", "11.121.326-1", "cl_tin")] + [BitAutoData("CL", "11121326-1", "cl_tin")] + [BitAutoData("CL", "23.121.326-K", "cl_tin")] + [BitAutoData("CL", "43651326-K", "cl_tin")] + [BitAutoData("CN", "123456789012345678", "cn_tin")] + [BitAutoData("CN", "123456789012345", "cn_tin")] + [BitAutoData("CO", "123.456.789-0", "co_nit")] + [BitAutoData("CO", "1234567890", "co_nit")] + [BitAutoData("CR", "1-234-567890", "cr_tin")] + [BitAutoData("CR", "1234567890", "cr_tin")] + [BitAutoData("HR", "HR12345678912", "eu_vat")] + [BitAutoData("HR", "12345678901", "hr_oib")] + [BitAutoData("CY", "CY12345678X", "eu_vat")] + [BitAutoData("CZ", "CZ12345678", "eu_vat")] + [BitAutoData("DK", "DK12345678", "eu_vat")] + [BitAutoData("DO", "123-4567890-1", "do_rcn")] + [BitAutoData("DO", "12345678901", "do_rcn")] + [BitAutoData("EC", "1234567890001", "ec_ruc")] + [BitAutoData("EG", "123456789", "eg_tin")] + [BitAutoData("SV", "1234-567890-123-4", "sv_nit")] + [BitAutoData("SV", "12345678901234", "sv_nit")] + [BitAutoData("EE", "EE123456789", "eu_vat")] + [BitAutoData("EU", "EU123456789", "eu_oss_vat")] + [BitAutoData("FI", "FI12345678", "eu_vat")] + [BitAutoData("FR", "FR12345678901", "eu_vat")] + [BitAutoData("GE", "123456789", "ge_vat")] + [BitAutoData("DE", "1234567890", "de_stn")] + [BitAutoData("DE", "DE123456789", "eu_vat")] + [BitAutoData("GR", "EL123456789", "eu_vat")] + [BitAutoData("HK", "12345678", "hk_br")] + [BitAutoData("HU", "HU12345678", "eu_vat")] + [BitAutoData("HU", "12345678-1-23", "hu_tin")] + [BitAutoData("HU", "12345678123", "hu_tin")] + [BitAutoData("IS", "123456", "is_vat")] + [BitAutoData("IN", "12ABCDE1234F1Z5", "in_gst")] + [BitAutoData("IN", "12ABCDE3456FGZH", "in_gst")] + [BitAutoData("ID", "012.345.678.9-012.345", "id_npwp")] + [BitAutoData("ID", "0123456789012345", "id_npwp")] + [BitAutoData("IE", "IE1234567A", "eu_vat")] + [BitAutoData("IE", "IE1234567AB", "eu_vat")] + [BitAutoData("IL", "000012345", "il_vat")] + [BitAutoData("IL", "123456789", "il_vat")] + [BitAutoData("IT", "IT12345678901", "eu_vat")] + [BitAutoData("JP", "1234567890123", "jp_cn")] + [BitAutoData("JP", "12345", "jp_rn")] + [BitAutoData("KZ", "123456789012", "kz_bin")] + [BitAutoData("KE", "P000111111A", "ke_pin")] + [BitAutoData("LV", "LV12345678912", "eu_vat")] + [BitAutoData("LI", "CHE123456789", "li_uid")] + [BitAutoData("LI", "12345", "li_vat")] + [BitAutoData("LT", "LT123456789123", "eu_vat")] + [BitAutoData("LU", "LU12345678", "eu_vat")] + [BitAutoData("MY", "12345678", "my_frp")] + [BitAutoData("MY", "C 1234567890", "my_itn")] + [BitAutoData("MY", "C1234567890", "my_itn")] + [BitAutoData("MY", "A12-3456-78912345", "my_sst")] + [BitAutoData("MY", "A12345678912345", "my_sst")] + [BitAutoData("MT", "MT12345678", "eu_vat")] + [BitAutoData("MX", "ABC010203AB9", "mx_rfc")] + [BitAutoData("MD", "1003600", "md_vat")] + [BitAutoData("MA", "12345678", "ma_vat")] + [BitAutoData("NL", "NL123456789B12", "eu_vat")] + [BitAutoData("NZ", "123456789", "nz_gst")] + [BitAutoData("NG", "12345678-0001", "ng_tin")] + [BitAutoData("NO", "123456789MVA", "no_vat")] + [BitAutoData("NO", "1234567", "no_voec")] + [BitAutoData("OM", "OM1234567890", "om_vat")] + [BitAutoData("PE", "12345678901", "pe_ruc")] + [BitAutoData("PH", "123456789012", "ph_tin")] + [BitAutoData("PL", "PL1234567890", "eu_vat")] + [BitAutoData("PT", "PT123456789", "eu_vat")] + [BitAutoData("RO", "RO1234567891", "eu_vat")] + [BitAutoData("RO", "1234567890123", "ro_tin")] + [BitAutoData("RU", "1234567891", "ru_inn")] + [BitAutoData("RU", "123456789", "ru_kpp")] + [BitAutoData("SA", "123456789012345", "sa_vat")] + [BitAutoData("RS", "123456789", "rs_pib")] + [BitAutoData("SG", "M12345678X", "sg_gst")] + [BitAutoData("SG", "123456789F", "sg_uen")] + [BitAutoData("SK", "SK1234567891", "eu_vat")] + [BitAutoData("SI", "SI12345678", "eu_vat")] + [BitAutoData("SI", "12345678", "si_tin")] + [BitAutoData("ZA", "4123456789", "za_vat")] + [BitAutoData("KR", "123-45-67890", "kr_brn")] + [BitAutoData("KR", "1234567890", "kr_brn")] + [BitAutoData("ES", "A12345678", "es_cif")] + [BitAutoData("ES", "ESX1234567X", "eu_vat")] + [BitAutoData("SE", "SE123456789012", "eu_vat")] + [BitAutoData("CH", "CHE-123.456.789 HR", "ch_uid")] + [BitAutoData("CH", "CHE123456789HR", "ch_uid")] + [BitAutoData("CH", "CHE-123.456.789 MWST", "ch_vat")] + [BitAutoData("CH", "CHE123456789MWST", "ch_vat")] + [BitAutoData("TW", "12345678", "tw_vat")] + [BitAutoData("TH", "1234567890123", "th_vat")] + [BitAutoData("TR", "0123456789", "tr_tin")] + [BitAutoData("UA", "123456789", "ua_vat")] + [BitAutoData("AE", "123456789012345", "ae_trn")] + [BitAutoData("GB", "XI123456789", "eu_vat")] + [BitAutoData("GB", "GB123456789", "gb_vat")] + [BitAutoData("US", "12-3456789", "us_ein")] + [BitAutoData("UY", "123456789012", "uy_ruc")] + [BitAutoData("UZ", "123456789", "uz_tin")] + [BitAutoData("UZ", "123456789012", "uz_vat")] + [BitAutoData("VE", "A-12345678-9", "ve_rif")] + [BitAutoData("VE", "A123456789", "ve_rif")] + [BitAutoData("VN", "1234567890", "vn_tin")] + public void GetStripeTaxCode_WithValidCountryAndTaxId_ReturnsExpectedTaxIdType( + string country, + string taxId, + string expected, + SutProvider sutProvider) + { + var result = sutProvider.Sut.GetStripeTaxCode(country, taxId); + + Assert.Equal(expected, result); + } +} diff --git a/src/Api/Billing/Controllers/AccountsBillingController.cs b/src/Api/Billing/Controllers/AccountsBillingController.cs index 574ac3e65e..fcb89226e7 100644 --- a/src/Api/Billing/Controllers/AccountsBillingController.cs +++ b/src/Api/Billing/Controllers/AccountsBillingController.cs @@ -1,5 +1,6 @@ #nullable enable using Bit.Api.Billing.Models.Responses; +using Bit.Core.Billing.Models.Api.Requests.Accounts; using Bit.Core.Billing.Services; using Bit.Core.Services; using Bit.Core.Utilities; @@ -77,4 +78,18 @@ public class AccountsBillingController( return TypedResults.Ok(transactions); } + + [HttpPost("preview-invoice")] + public async Task PreviewInvoiceAsync([FromBody] PreviewIndividualInvoiceRequestBody model) + { + var user = await userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + var invoice = await paymentService.PreviewInvoiceAsync(model, user.GatewayCustomerId, user.GatewaySubscriptionId); + + return TypedResults.Ok(invoice); + } } diff --git a/src/Api/Billing/Controllers/InvoicesController.cs b/src/Api/Billing/Controllers/InvoicesController.cs new file mode 100644 index 0000000000..686d9b9643 --- /dev/null +++ b/src/Api/Billing/Controllers/InvoicesController.cs @@ -0,0 +1,42 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Models.Api.Requests.Organizations; +using Bit.Core.Context; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Billing.Controllers; + +[Route("invoices")] +[Authorize("Application")] +public class InvoicesController : BaseBillingController +{ + [HttpPost("preview-organization")] + public async Task PreviewInvoiceAsync( + [FromBody] PreviewOrganizationInvoiceRequestBody model, + [FromServices] ICurrentContext currentContext, + [FromServices] IOrganizationRepository organizationRepository, + [FromServices] IPaymentService paymentService) + { + Organization organization = null; + if (model.OrganizationId != default) + { + if (!await currentContext.EditPaymentMethods(model.OrganizationId)) + { + return Error.Unauthorized(); + } + + organization = await organizationRepository.GetByIdAsync(model.OrganizationId); + if (organization == null) + { + return Error.NotFound(); + } + } + + var invoice = await paymentService.PreviewInvoiceAsync(model, organization?.GatewayCustomerId, + organization?.GatewaySubscriptionId); + + return TypedResults.Ok(invoice); + } +} diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index f7ddf0853e..c5de63c69b 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -119,6 +119,7 @@ public class ProviderBillingController( requestBody.Country, requestBody.PostalCode, requestBody.TaxId, + requestBody.TaxIdType, requestBody.Line1, requestBody.Line2, requestBody.City, diff --git a/src/Api/Billing/Controllers/StripeController.cs b/src/Api/Billing/Controllers/StripeController.cs index a4a974bb99..f5e8253bfa 100644 --- a/src/Api/Billing/Controllers/StripeController.cs +++ b/src/Api/Billing/Controllers/StripeController.cs @@ -1,4 +1,5 @@ -using Bit.Core.Services; +using Bit.Core.Billing.Services; +using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -46,4 +47,15 @@ public class StripeController( return TypedResults.Ok(setupIntent.ClientSecret); } + + [HttpGet] + [Route("~/tax/is-country-supported")] + public IResult IsCountrySupported( + [FromQuery] string country, + [FromServices] ITaxService taxService) + { + var isSupported = taxService.IsSupported(country); + + return TypedResults.Ok(isSupported); + } } diff --git a/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs b/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs index c5c0fde00b..32ba2effb2 100644 --- a/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs +++ b/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs @@ -10,6 +10,7 @@ public class TaxInformationRequestBody [Required] public string PostalCode { get; set; } public string TaxId { get; set; } + public string TaxIdType { get; set; } public string Line1 { get; set; } public string Line2 { get; set; } public string City { get; set; } @@ -19,6 +20,7 @@ public class TaxInformationRequestBody Country, PostalCode, TaxId, + TaxIdType, Line1, Line2, City, diff --git a/src/Core/Billing/Extensions/CurrencyExtensions.cs b/src/Core/Billing/Extensions/CurrencyExtensions.cs new file mode 100644 index 0000000000..cde1a7bea8 --- /dev/null +++ b/src/Core/Billing/Extensions/CurrencyExtensions.cs @@ -0,0 +1,33 @@ +namespace Bit.Core.Billing.Extensions; + +public static class CurrencyExtensions +{ + /// + /// Converts a currency amount in major units to minor units. + /// + /// 123.99 USD returns 12399 in minor units. + public static long ToMinor(this decimal amount) + { + return Convert.ToInt64(amount * 100); + } + + /// + /// Converts a currency amount in minor units to major units. + /// + /// + /// 12399 in minor units returns 123.99 USD. + public static decimal? ToMajor(this long? amount) + { + return amount?.ToMajor(); + } + + /// + /// Converts a currency amount in minor units to major units. + /// + /// + /// 12399 in minor units returns 123.99 USD. + public static decimal ToMajor(this long amount) + { + return Convert.ToDecimal(amount) / 100; + } +} diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index abfceac736..8f2803d920 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ public static class ServiceCollectionExtensions { public static void AddBillingOperations(this IServiceCollection services) { + services.AddSingleton(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs b/src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs new file mode 100644 index 0000000000..6dfb9894d5 --- /dev/null +++ b/src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Billing.Models.Api.Requests.Accounts; + +public class PreviewIndividualInvoiceRequestBody +{ + [Required] + public PasswordManagerRequestModel PasswordManager { get; set; } + + [Required] + public TaxInformationRequestModel TaxInformation { get; set; } +} + +public class PasswordManagerRequestModel +{ + [Range(0, int.MaxValue)] + public int AdditionalStorage { get; set; } +} diff --git a/src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs b/src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs new file mode 100644 index 0000000000..18d9c352d7 --- /dev/null +++ b/src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Billing.Enums; + +namespace Bit.Core.Billing.Models.Api.Requests.Organizations; + +public class PreviewOrganizationInvoiceRequestBody +{ + public Guid OrganizationId { get; set; } + + [Required] + public PasswordManagerRequestModel PasswordManager { get; set; } + + public SecretsManagerRequestModel SecretsManager { get; set; } + + [Required] + public TaxInformationRequestModel TaxInformation { get; set; } +} + +public class PasswordManagerRequestModel +{ + public PlanType Plan { get; set; } + + [Range(0, int.MaxValue)] + public int Seats { get; set; } + + [Range(0, int.MaxValue)] + public int AdditionalStorage { get; set; } +} + +public class SecretsManagerRequestModel +{ + [Range(0, int.MaxValue)] + public int Seats { get; set; } + + [Range(0, int.MaxValue)] + public int AdditionalMachineAccounts { get; set; } +} diff --git a/src/Core/Billing/Models/Api/Requests/TaxInformationRequestModel.cs b/src/Core/Billing/Models/Api/Requests/TaxInformationRequestModel.cs new file mode 100644 index 0000000000..9cb43645c6 --- /dev/null +++ b/src/Core/Billing/Models/Api/Requests/TaxInformationRequestModel.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Billing.Models.Api.Requests; + +public class TaxInformationRequestModel +{ + [Length(2, 2), Required] + public string Country { get; set; } + + [Required] + public string PostalCode { get; set; } + + public string TaxId { get; set; } +} diff --git a/src/Core/Billing/Models/Api/Responses/PreviewInvoiceResponseModel.cs b/src/Core/Billing/Models/Api/Responses/PreviewInvoiceResponseModel.cs new file mode 100644 index 0000000000..fdde7dae1e --- /dev/null +++ b/src/Core/Billing/Models/Api/Responses/PreviewInvoiceResponseModel.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Billing.Models.Api.Responses; + +public record PreviewInvoiceResponseModel( + decimal EffectiveTaxRate, + decimal TaxableBaseAmount, + decimal TaxAmount, + decimal TotalAmount); diff --git a/src/Core/Billing/Models/PreviewInvoiceInfo.cs b/src/Core/Billing/Models/PreviewInvoiceInfo.cs new file mode 100644 index 0000000000..16a2019c20 --- /dev/null +++ b/src/Core/Billing/Models/PreviewInvoiceInfo.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Billing.Models; + +public record PreviewInvoiceInfo( + decimal EffectiveTaxRate, + decimal TaxableBaseAmount, + decimal TaxAmount, + decimal TotalAmount); diff --git a/src/Core/Billing/Models/Sales/OrganizationSale.cs b/src/Core/Billing/Models/Sales/OrganizationSale.cs index a19c278c68..43852bb320 100644 --- a/src/Core/Billing/Models/Sales/OrganizationSale.cs +++ b/src/Core/Billing/Models/Sales/OrganizationSale.cs @@ -65,6 +65,7 @@ public class OrganizationSale signup.TaxInfo.BillingAddressCountry, signup.TaxInfo.BillingAddressPostalCode, signup.TaxInfo.TaxIdNumber, + signup.TaxInfo.TaxIdType, signup.TaxInfo.BillingAddressLine1, signup.TaxInfo.BillingAddressLine2, signup.TaxInfo.BillingAddressCity, diff --git a/src/Core/Billing/Models/TaxIdType.cs b/src/Core/Billing/Models/TaxIdType.cs new file mode 100644 index 0000000000..3fc246d68b --- /dev/null +++ b/src/Core/Billing/Models/TaxIdType.cs @@ -0,0 +1,22 @@ +using System.Text.RegularExpressions; + +namespace Bit.Core.Billing.Models; + +public class TaxIdType +{ + /// + /// ISO-3166-2 code for the country. + /// + public string Country { get; set; } + + /// + /// The identifier in Stripe for the tax ID type. + /// + public string Code { get; set; } + + public Regex ValidationExpression { get; set; } + + public string Description { get; set; } + + public string Example { get; set; } +} diff --git a/src/Core/Billing/Models/TaxInformation.cs b/src/Core/Billing/Models/TaxInformation.cs index 5403f94690..23ed3e5faa 100644 --- a/src/Core/Billing/Models/TaxInformation.cs +++ b/src/Core/Billing/Models/TaxInformation.cs @@ -1,5 +1,4 @@ using Bit.Core.Models.Business; -using Stripe; namespace Bit.Core.Billing.Models; @@ -7,6 +6,7 @@ public record TaxInformation( string Country, string PostalCode, string TaxId, + string TaxIdType, string Line1, string Line2, string City, @@ -16,165 +16,9 @@ public record TaxInformation( taxInfo.BillingAddressCountry, taxInfo.BillingAddressPostalCode, taxInfo.TaxIdNumber, + taxInfo.TaxIdType, taxInfo.BillingAddressLine1, taxInfo.BillingAddressLine2, taxInfo.BillingAddressCity, taxInfo.BillingAddressState); - - public (AddressOptions, List) GetStripeOptions() - { - var address = new AddressOptions - { - Country = Country, - PostalCode = PostalCode, - Line1 = Line1, - Line2 = Line2, - City = City, - State = State - }; - - var customerTaxIdDataOptionsList = !string.IsNullOrEmpty(TaxId) - ? new List { new() { Type = GetTaxIdType(), Value = TaxId } } - : null; - - return (address, customerTaxIdDataOptionsList); - } - - public string GetTaxIdType() - { - if (string.IsNullOrEmpty(Country) || string.IsNullOrEmpty(TaxId)) - { - return null; - } - - switch (Country.ToUpper()) - { - case "AD": - return "ad_nrt"; - case "AE": - return "ae_trn"; - case "AR": - return "ar_cuit"; - case "AU": - return "au_abn"; - case "BO": - return "bo_tin"; - case "BR": - return "br_cnpj"; - case "CA": - // May break for those in Québec given the assumption of QST - if (State?.Contains("bec") ?? false) - { - return "ca_qst"; - } - return "ca_bn"; - case "CH": - return "ch_vat"; - case "CL": - return "cl_tin"; - case "CN": - return "cn_tin"; - case "CO": - return "co_nit"; - case "CR": - return "cr_tin"; - case "DO": - return "do_rcn"; - case "EC": - return "ec_ruc"; - case "EG": - return "eg_tin"; - case "GE": - return "ge_vat"; - case "ID": - return "id_npwp"; - case "IL": - return "il_vat"; - case "IS": - return "is_vat"; - case "KE": - return "ke_pin"; - case "AT": - case "BE": - case "BG": - case "CY": - case "CZ": - case "DE": - case "DK": - case "EE": - case "ES": - case "FI": - case "FR": - case "GB": - case "GR": - case "HR": - case "HU": - case "IE": - case "IT": - case "LT": - case "LU": - case "LV": - case "MT": - case "NL": - case "PL": - case "PT": - case "RO": - case "SE": - case "SI": - case "SK": - return "eu_vat"; - case "HK": - return "hk_br"; - case "IN": - return "in_gst"; - case "JP": - return "jp_cn"; - case "KR": - return "kr_brn"; - case "LI": - return "li_uid"; - case "MX": - return "mx_rfc"; - case "MY": - return "my_sst"; - case "NO": - return "no_vat"; - case "NZ": - return "nz_gst"; - case "PE": - return "pe_ruc"; - case "PH": - return "ph_tin"; - case "RS": - return "rs_pib"; - case "RU": - return "ru_inn"; - case "SA": - return "sa_vat"; - case "SG": - return "sg_gst"; - case "SV": - return "sv_nit"; - case "TH": - return "th_vat"; - case "TR": - return "tr_tin"; - case "TW": - return "tw_vat"; - case "UA": - return "ua_vat"; - case "US": - return "us_ein"; - case "UY": - return "uy_ruc"; - case "VE": - return "ve_rif"; - case "VN": - return "vn_tin"; - case "ZA": - return "za_vat"; - default: - return null; - } - } } diff --git a/src/Core/Billing/Services/ITaxService.cs b/src/Core/Billing/Services/ITaxService.cs new file mode 100644 index 0000000000..beee113d17 --- /dev/null +++ b/src/Core/Billing/Services/ITaxService.cs @@ -0,0 +1,22 @@ +namespace Bit.Core.Billing.Services; + +public interface ITaxService +{ + /// + /// Retrieves the Stripe tax code for a given country and tax ID. + /// + /// + /// + /// + /// Returns the Stripe tax code if the tax ID is valid for the country. + /// Returns null if the tax ID is invalid or the country is not supported. + /// + string GetStripeTaxCode(string country, string taxId); + + /// + /// Returns true or false whether charging or storing tax is supported for the given country. + /// + /// + /// + bool IsSupported(string country); +} diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index eadc589625..b186a99d93 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -28,7 +28,8 @@ public class OrganizationBillingService( IOrganizationRepository organizationRepository, ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, - ISubscriberService subscriberService) : IOrganizationBillingService + ISubscriberService subscriberService, + ITaxService taxService) : IOrganizationBillingService { public async Task Finalize(OrganizationSale sale) { @@ -167,14 +168,38 @@ public class OrganizationBillingService( throw new BillingException(); } - var (address, taxIdData) = customerSetup.TaxInformation.GetStripeOptions(); - - customerCreateOptions.Address = address; + customerCreateOptions.Address = new AddressOptions + { + Line1 = customerSetup.TaxInformation.Line1, + Line2 = customerSetup.TaxInformation.Line2, + City = customerSetup.TaxInformation.City, + PostalCode = customerSetup.TaxInformation.PostalCode, + State = customerSetup.TaxInformation.State, + Country = customerSetup.TaxInformation.Country, + }; customerCreateOptions.Tax = new CustomerTaxOptions { ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately }; - customerCreateOptions.TaxIdData = taxIdData; + + if (!string.IsNullOrEmpty(customerSetup.TaxInformation.TaxId)) + { + var taxIdType = taxService.GetStripeTaxCode(customerSetup.TaxInformation.Country, + customerSetup.TaxInformation.TaxId); + + if (taxIdType == null) + { + logger.LogWarning("Could not determine tax ID type for organization '{OrganizationID}' in country '{Country}' with tax ID '{TaxID}'.", + organization.Id, + customerSetup.TaxInformation.Country, + customerSetup.TaxInformation.TaxId); + } + + customerCreateOptions.TaxIdData = + [ + new() { Type = taxIdType, Value = customerSetup.TaxInformation.TaxId } + ]; + } var (paymentMethodType, paymentMethodToken) = customerSetup.TokenizedPaymentSource; diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index 92c81dae1c..5351888ad9 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -24,7 +24,8 @@ public class PremiumUserBillingService( ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, ISubscriberService subscriberService, - IUserRepository userRepository) : IPremiumUserBillingService + IUserRepository userRepository, + ITaxService taxService) : IPremiumUserBillingService { public async Task Finalize(PremiumUserSale sale) { @@ -82,13 +83,19 @@ public class PremiumUserBillingService( throw new BillingException(); } - var (address, taxIdData) = customerSetup.TaxInformation.GetStripeOptions(); - var subscriberName = user.SubscriberName(); var customerCreateOptions = new CustomerCreateOptions { - Address = address, + Address = new AddressOptions + { + Line1 = customerSetup.TaxInformation.Line1, + Line2 = customerSetup.TaxInformation.Line2, + City = customerSetup.TaxInformation.City, + PostalCode = customerSetup.TaxInformation.PostalCode, + State = customerSetup.TaxInformation.State, + Country = customerSetup.TaxInformation.Country, + }, Description = user.Name, Email = user.Email, Expand = ["tax"], @@ -113,10 +120,28 @@ public class PremiumUserBillingService( Tax = new CustomerTaxOptions { ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately - }, - TaxIdData = taxIdData + } }; + if (!string.IsNullOrEmpty(customerSetup.TaxInformation.TaxId)) + { + var taxIdType = taxService.GetStripeTaxCode(customerSetup.TaxInformation.Country, + customerSetup.TaxInformation.TaxId); + + if (taxIdType == null) + { + logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", + customerSetup.TaxInformation.Country, + customerSetup.TaxInformation.TaxId); + throw new Exceptions.BadRequestException("billingTaxIdTypeInferenceError"); + } + + customerCreateOptions.TaxIdData = + [ + new() { Type = taxIdType, Value = customerSetup.TaxInformation.TaxId } + ]; + } + var (paymentMethodType, paymentMethodToken) = customerSetup.TokenizedPaymentSource; var braintreeCustomerId = ""; diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index 9b8f64be82..6125d15419 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -23,7 +23,8 @@ public class SubscriberService( IGlobalSettings globalSettings, ILogger logger, ISetupIntentCache setupIntentCache, - IStripeAdapter stripeAdapter) : ISubscriberService + IStripeAdapter stripeAdapter, + ITaxService taxService) : ISubscriberService { public async Task CancelSubscription( ISubscriber subscriber, @@ -618,16 +619,47 @@ public class SubscriberService( await stripeAdapter.TaxIdDeleteAsync(customer.Id, taxId.Id); } - var taxIdType = taxInformation.GetTaxIdType(); - - if (!string.IsNullOrWhiteSpace(taxInformation.TaxId) && - !string.IsNullOrWhiteSpace(taxIdType)) + if (string.IsNullOrWhiteSpace(taxInformation.TaxId)) { - await stripeAdapter.TaxIdCreateAsync(customer.Id, new TaxIdCreateOptions + return; + } + + var taxIdType = taxInformation.TaxIdType; + if (string.IsNullOrWhiteSpace(taxIdType)) + { + taxIdType = taxService.GetStripeTaxCode(taxInformation.Country, + taxInformation.TaxId); + + if (taxIdType == null) { - Type = taxIdType, - Value = taxInformation.TaxId, - }); + logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", + taxInformation.Country, + taxInformation.TaxId); + throw new Exceptions.BadRequestException("billingTaxIdTypeInferenceError"); + } + } + + try + { + await stripeAdapter.TaxIdCreateAsync(customer.Id, + new TaxIdCreateOptions { Type = taxIdType, Value = taxInformation.TaxId }); + } + catch (StripeException e) + { + switch (e.StripeError.Code) + { + case StripeConstants.ErrorCodes.TaxIdInvalid: + logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", + taxInformation.TaxId, + taxInformation.Country); + throw new Exceptions.BadRequestException("billingInvalidTaxIdError"); + default: + logger.LogError(e, "Error creating tax ID '{TaxId}' in country '{Country}' for customer '{CustomerID}'.", + taxInformation.TaxId, + taxInformation.Country, + customer.Id); + throw new Exceptions.BadRequestException("billingTaxIdCreationError"); + } } } @@ -770,6 +802,7 @@ public class SubscriberService( customer.Address.Country, customer.Address.PostalCode, customer.TaxIds?.FirstOrDefault()?.Value, + customer.TaxIds?.FirstOrDefault()?.Type, customer.Address.Line1, customer.Address.Line2, customer.Address.City, diff --git a/src/Core/Billing/Services/TaxService.cs b/src/Core/Billing/Services/TaxService.cs new file mode 100644 index 0000000000..3066be92d1 --- /dev/null +++ b/src/Core/Billing/Services/TaxService.cs @@ -0,0 +1,901 @@ +using System.Text.RegularExpressions; +using Bit.Core.Billing.Models; + +namespace Bit.Core.Billing.Services; + +public class TaxService : ITaxService +{ + /// + /// Retrieves a list of supported tax ID types for customers. + /// + /// Compiled list from Stripe + private static readonly IEnumerable _taxIdTypes = + [ + new() + { + Country = "AD", + Code = "ad_nrt", + Description = "Andorran NRT number", + Example = "A-123456-Z", + ValidationExpression = new Regex("^([A-Z]{1})-?([0-9]{6})-?([A-Z]{1})$") + }, + new() + { + Country = "AR", + Code = "ar_cuit", + Description = "Argentinian tax ID number", + Example = "12-34567890-1", + ValidationExpression = new Regex("^([0-9]{2})-?([0-9]{8})-?([0-9]{1})$") + }, + new() + { + Country = "AU", + Code = "au_abn", + Description = "Australian Business Number (AU ABN)", + Example = "123456789012", + ValidationExpression = new Regex("^[0-9]{11}$") + }, + new() + { + Country = "AU", + Code = "au_arn", + Description = "Australian Taxation Office Reference Number", + Example = "123456789123", + ValidationExpression = new Regex("^[0-9]{12}$") + }, + new() + { + Country = "AT", + Code = "eu_vat", + Description = "European VAT number (Austria)", + Example = "ATU12345678", + ValidationExpression = new Regex("^ATU[0-9]{8}$") + }, + new() + { + Country = "BH", + Code = "bh_vat", + Description = "Bahraini VAT Number", + Example = "123456789012345", + ValidationExpression = new Regex("^[0-9]{15}$") + }, + new() + { + Country = "BY", + Code = "by_tin", + Description = "Belarus TIN Number", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "BE", + Code = "eu_vat", + Description = "European VAT number (Belgium)", + Example = "BE0123456789", + ValidationExpression = new Regex("^BE[0-9]{10}$") + }, + new() + { + Country = "BO", + Code = "bo_tin", + Description = "Bolivian tax ID", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "BR", + Code = "br_cnpj", + Description = "Brazilian CNPJ number", + Example = "01.234.456/5432-10", + ValidationExpression = new Regex("^[0-9]{2}.?[0-9]{3}.?[0-9]{3}/?[0-9]{4}-?[0-9]{2}$") + }, + new() + { + Country = "BR", + Code = "br_cpf", + Description = "Brazilian CPF number", + Example = "123.456.789-87", + ValidationExpression = new Regex("^[0-9]{3}.?[0-9]{3}.?[0-9]{3}-?[0-9]{2}$") + }, + new() + { + Country = "BG", + Code = "bg_uic", + Description = "Bulgaria Unified Identification Code", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "BG", + Code = "eu_vat", + Description = "European VAT number (Bulgaria)", + Example = "BG0123456789", + ValidationExpression = new Regex("^BG[0-9]{9,10}$") + }, + new() + { + Country = "CA", + Code = "ca_bn", + Description = "Canadian BN", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "CA", + Code = "ca_gst_hst", + Description = "Canadian GST/HST number", + Example = "123456789RT0002", + ValidationExpression = new Regex("^[0-9]{9}RT[0-9]{4}$") + }, + new() + { + Country = "CA", + Code = "ca_pst_bc", + Description = "Canadian PST number (British Columbia)", + Example = "PST-1234-5678", + ValidationExpression = new Regex("^PST-[0-9]{4}-[0-9]{4}$") + }, + new() + { + Country = "CA", + Code = "ca_pst_mb", + Description = "Canadian PST number (Manitoba)", + Example = "123456-7", + ValidationExpression = new Regex("^[0-9]{6}-[0-9]{1}$") + }, + new() + { + Country = "CA", + Code = "ca_pst_sk", + Description = "Canadian PST number (Saskatchewan)", + Example = "1234567", + ValidationExpression = new Regex("^[0-9]{7}$") + }, + new() + { + Country = "CA", + Code = "ca_qst", + Description = "Canadian QST number (Québec)", + Example = "1234567890TQ1234", + ValidationExpression = new Regex("^[0-9]{10}TQ[0-9]{4}$") + }, + new() + { + Country = "CL", + Code = "cl_tin", + Description = "Chilean TIN", + Example = "12.345.678-K", + ValidationExpression = new Regex("^[0-9]{2}.?[0-9]{3}.?[0-9]{3}-?[0-9A-Z]{1}$") + }, + new() + { + Country = "CN", + Code = "cn_tin", + Description = "Chinese tax ID", + Example = "123456789012345678", + ValidationExpression = new Regex("^[0-9]{15,18}$") + }, + new() + { + Country = "CO", + Code = "co_nit", + Description = "Colombian NIT number", + Example = "123.456.789-0", + ValidationExpression = new Regex("^[0-9]{3}.?[0-9]{3}.?[0-9]{3}-?[0-9]{1}$") + }, + new() + { + Country = "CR", + Code = "cr_tin", + Description = "Costa Rican tax ID", + Example = "1-234-567890", + ValidationExpression = new Regex("^[0-9]{1}-?[0-9]{3}-?[0-9]{6}$") + }, + new() + { + Country = "HR", + Code = "eu_vat", + Description = "European VAT number (Croatia)", + Example = "HR12345678912", + ValidationExpression = new Regex("^HR[0-9]{11}$") + }, + new() + { + Country = "HR", + Code = "hr_oib", + Description = "Croatian Personal Identification Number", + Example = "12345678901", + ValidationExpression = new Regex("^[0-9]{11}$") + }, + new() + { + Country = "CY", + Code = "eu_vat", + Description = "European VAT number (Cyprus)", + Example = "CY12345678X", + ValidationExpression = new Regex("^CY[0-9]{8}[A-Z]{1}$") + }, + new() + { + Country = "CZ", + Code = "eu_vat", + Description = "European VAT number (Czech Republic)", + Example = "CZ12345678", + ValidationExpression = new Regex("^CZ[0-9]{8,10}$") + }, + new() + { + Country = "DK", + Code = "eu_vat", + Description = "European VAT number (Denmark)", + Example = "DK12345678", + ValidationExpression = new Regex("^DK[0-9]{8}$") + }, + new() + { + Country = "DO", + Code = "do_rcn", + Description = "Dominican RCN number", + Example = "123-4567890-1", + ValidationExpression = new Regex("^[0-9]{3}-?[0-9]{7}-?[0-9]{1}$") + }, + new() + { + Country = "EC", + Code = "ec_ruc", + Description = "Ecuadorian RUC number", + Example = "1234567890001", + ValidationExpression = new Regex("^[0-9]{13}$") + }, + new() + { + Country = "EG", + Code = "eg_tin", + Description = "Egyptian Tax Identification Number", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + + new() + { + Country = "SV", + Code = "sv_nit", + Description = "El Salvadorian NIT number", + Example = "1234-567890-123-4", + ValidationExpression = new Regex("^[0-9]{4}-?[0-9]{6}-?[0-9]{3}-?[0-9]{1}$") + }, + + new() + { + Country = "EE", + Code = "eu_vat", + Description = "European VAT number (Estonia)", + Example = "EE123456789", + ValidationExpression = new Regex("^EE[0-9]{9}$") + }, + + new() + { + Country = "EU", + Code = "eu_oss_vat", + Description = "European One Stop Shop VAT number for non-Union scheme", + Example = "EU123456789", + ValidationExpression = new Regex("^EU[0-9]{9}$") + }, + new() + { + Country = "FI", + Code = "eu_vat", + Description = "European VAT number (Finland)", + Example = "FI12345678", + ValidationExpression = new Regex("^FI[0-9]{8}$") + }, + new() + { + Country = "FR", + Code = "eu_vat", + Description = "European VAT number (France)", + Example = "FR12345678901", + ValidationExpression = new Regex("^FR[0-9A-Z]{2}[0-9]{9}$") + }, + new() + { + Country = "GE", + Code = "ge_vat", + Description = "Georgian VAT", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "DE", + Code = "de_stn", + Description = "German Tax Number (Steuernummer)", + Example = "1234567890", + ValidationExpression = new Regex("^[0-9]{10}$") + }, + new() + { + Country = "DE", + Code = "eu_vat", + Description = "European VAT number (Germany)", + Example = "DE123456789", + ValidationExpression = new Regex("^DE[0-9]{9}$") + }, + new() + { + Country = "GR", + Code = "eu_vat", + Description = "European VAT number (Greece)", + Example = "EL123456789", + ValidationExpression = new Regex("^EL[0-9]{9}$") + }, + new() + { + Country = "HK", + Code = "hk_br", + Description = "Hong Kong BR number", + Example = "12345678", + ValidationExpression = new Regex("^[0-9]{8}$") + }, + new() + { + Country = "HU", + Code = "eu_vat", + Description = "European VAT number (Hungaria)", + Example = "HU12345678", + ValidationExpression = new Regex("^HU[0-9]{8}$") + }, + new() + { + Country = "HU", + Code = "hu_tin", + Description = "Hungary tax number (adószám)", + Example = "12345678-1-23", + ValidationExpression = new Regex("^[0-9]{8}-?[0-9]-?[0-9]{2}$") + }, + new() + { + Country = "IS", + Code = "is_vat", + Description = "Icelandic VAT", + Example = "123456", + ValidationExpression = new Regex("^[0-9]{6}$") + }, + new() + { + Country = "IN", + Code = "in_gst", + Description = "Indian GST number", + Example = "12ABCDE3456FGZH", + ValidationExpression = new Regex("^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$") + }, + new() + { + Country = "ID", + Code = "id_npwp", + Description = "Indonesian NPWP number", + Example = "012.345.678.9-012.345", + ValidationExpression = new Regex("^[0-9]{3}.?[0-9]{3}.?[0-9]{3}.?[0-9]{1}-?[0-9]{3}.?[0-9]{3}$") + }, + new() + { + Country = "IE", + Code = "eu_vat", + Description = "European VAT number (Ireland)", + Example = "IE1234567AB", + ValidationExpression = new Regex("^IE[0-9]{7}[A-Z]{1,2}$") + }, + new() + { + Country = "IL", + Code = "il_vat", + Description = "Israel VAT", + Example = "000012345", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "IT", + Code = "eu_vat", + Description = "European VAT number (Italy)", + Example = "IT12345678912", + ValidationExpression = new Regex("^IT[0-9]{11}$") + }, + new() + { + Country = "JP", + Code = "jp_cn", + Description = "Japanese Corporate Number (*Hōjin Bangō*)", + Example = "1234567891234", + ValidationExpression = new Regex("^[0-9]{13}$") + }, + new() + { + Country = "JP", + Code = "jp_rn", + Description = + "Japanese Registered Foreign Businesses' Registration Number (*Tōroku Kokugai Jigyōsha no Tōroku Bangō*)", + Example = "12345", + ValidationExpression = new Regex("^[0-9]{5}$") + }, + new() + { + Country = "JP", + Code = "jp_trn", + Description = "Japanese Tax Registration Number (*Tōroku Bangō*)", + Example = "T1234567891234", + ValidationExpression = new Regex("^T[0-9]{13}$") + }, + new() + { + Country = "KZ", + Code = "kz_bin", + Description = "Kazakhstani Business Identification Number", + Example = "123456789012", + ValidationExpression = new Regex("^[0-9]{12}$") + }, + new() + { + Country = "KE", + Code = "ke_pin", + Description = "Kenya Revenue Authority Personal Identification Number", + Example = "P000111111A", + ValidationExpression = new Regex("^[A-Z]{1}[0-9]{9}[A-Z]{1}$") + }, + new() + { + Country = "LV", + Code = "eu_vat", + Description = "European VAT number", + Example = "LV12345678912", + ValidationExpression = new Regex("^LV[0-9]{11}$") + }, + new() + { + Country = "LI", + Code = "li_uid", + Description = "Liechtensteinian UID number", + Example = "CHE123456789", + ValidationExpression = new Regex("^CHE[0-9]{9}$") + }, + new() + { + Country = "LI", + Code = "li_vat", + Description = "Liechtensteinian VAT number", + Example = "12345", + ValidationExpression = new Regex("^[0-9]{5}$") + }, + new() + { + Country = "LT", + Code = "eu_vat", + Description = "European VAT number (Lithuania)", + Example = "LT123456789123", + ValidationExpression = new Regex("^LT[0-9]{9,12}$") + }, + new() + { + Country = "LU", + Code = "eu_vat", + Description = "European VAT number (Luxembourg)", + Example = "LU12345678", + ValidationExpression = new Regex("^LU[0-9]{8}$") + }, + new() + { + Country = "MY", + Code = "my_frp", + Description = "Malaysian FRP number", + Example = "12345678", + ValidationExpression = new Regex("^[0-9]{8}$") + }, + new() + { + Country = "MY", + Code = "my_itn", + Description = "Malaysian ITN", + Example = "C 1234567890", + ValidationExpression = new Regex("^[A-Z]{1} ?[0-9]{10}$") + }, + new() + { + Country = "MY", + Code = "my_sst", + Description = "Malaysian SST number", + Example = "A12-3456-78912345", + ValidationExpression = new Regex("^[A-Z]{1}[0-9]{2}-?[0-9]{4}-?[0-9]{8}$") + }, + new() + { + Country = "MT", + Code = "eu_vat", + Description = "European VAT number (Malta)", + Example = "MT12345678", + ValidationExpression = new Regex("^MT[0-9]{8}$") + }, + new() + { + Country = "MX", + Code = "mx_rfc", + Description = "Mexican RFC number", + Example = "ABC010203AB9", + ValidationExpression = new Regex("^[A-Z]{3}[0-9]{6}[A-Z0-9]{3}$") + }, + new() + { + Country = "MD", + Code = "md_vat", + Description = "Moldova VAT Number", + Example = "1234567", + ValidationExpression = new Regex("^[0-9]{7}$") + }, + new() + { + Country = "MA", + Code = "ma_vat", + Description = "Morocco VAT Number", + Example = "12345678", + ValidationExpression = new Regex("^[0-9]{8}$") + }, + new() + { + Country = "NL", + Code = "eu_vat", + Description = "European VAT number (Netherlands)", + Example = "NL123456789B12", + ValidationExpression = new Regex("^NL[0-9]{9}B[0-9]{2}$") + }, + new() + { + Country = "NZ", + Code = "nz_gst", + Description = "New Zealand GST number", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "NG", + Code = "ng_tin", + Description = "Nigerian TIN Number", + Example = "12345678-0001", + ValidationExpression = new Regex("^[0-9]{8}-[0-9]{4}$") + }, + new() + { + Country = "NO", + Code = "no_vat", + Description = "Norwegian VAT number", + Example = "123456789MVA", + ValidationExpression = new Regex("^[0-9]{9}MVA$") + }, + new() + { + Country = "NO", + Code = "no_voec", + Description = "Norwegian VAT on e-commerce number", + Example = "1234567", + ValidationExpression = new Regex("^[0-9]{7}$") + }, + new() + { + Country = "OM", + Code = "om_vat", + Description = "Omani VAT Number", + Example = "OM1234567890", + ValidationExpression = new Regex("^OM[0-9]{10}$") + }, + new() + { + Country = "PE", + Code = "pe_ruc", + Description = "Peruvian RUC number", + Example = "12345678901", + ValidationExpression = new Regex("^[0-9]{11}$") + }, + new() + { + Country = "PH", + Code = "ph_tin", + Description = "Philippines Tax Identification Number", + Example = "123456789012", + ValidationExpression = new Regex("^[0-9]{12}$") + }, + new() + { + Country = "PL", + Code = "eu_vat", + Description = "European VAT number (Poland)", + Example = "PL1234567890", + ValidationExpression = new Regex("^PL[0-9]{10}$") + }, + new() + { + Country = "PT", + Code = "eu_vat", + Description = "European VAT number (Portugal)", + Example = "PT123456789", + ValidationExpression = new Regex("^PT[0-9]{9}$") + }, + new() + { + Country = "RO", + Code = "eu_vat", + Description = "European VAT number (Romania)", + Example = "RO1234567891", + ValidationExpression = new Regex("^RO[0-9]{2,10}$") + }, + new() + { + Country = "RO", + Code = "ro_tin", + Description = "Romanian tax ID number", + Example = "1234567890123", + ValidationExpression = new Regex("^[0-9]{13}$") + }, + new() + { + Country = "RU", + Code = "ru_inn", + Description = "Russian INN", + Example = "1234567891", + ValidationExpression = new Regex("^[0-9]{10,12}$") + }, + new() + { + Country = "RU", + Code = "ru_kpp", + Description = "Russian KPP", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "SA", + Code = "sa_vat", + Description = "Saudi Arabia VAT", + Example = "123456789012345", + ValidationExpression = new Regex("^[0-9]{15}$") + }, + new() + { + Country = "RS", + Code = "rs_pib", + Description = "Serbian PIB number", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "SG", + Code = "sg_gst", + Description = "Singaporean GST", + Example = "M12345678X", + ValidationExpression = new Regex("^[A-Z]{1}[0-9]{8}[A-Z]{1}$") + }, + new() + { + Country = "SG", + Code = "sg_uen", + Description = "Singaporean UEN", + Example = "123456789F", + ValidationExpression = new Regex("^[0-9]{9}[A-Z]{1}$") + }, + new() + { + Country = "SK", + Code = "eu_vat", + Description = "European VAT number (Slovakia)", + Example = "SK1234567891", + ValidationExpression = new Regex("^SK[0-9]{10}$") + }, + new() + { + Country = "SI", + Code = "eu_vat", + Description = "European VAT number (Slovenia)", + Example = "SI12345678", + ValidationExpression = new Regex("^SI[0-9]{8}$") + }, + new() + { + Country = "SI", + Code = "si_tin", + Description = "Slovenia tax number (davčna številka)", + Example = "12345678", + ValidationExpression = new Regex("^[0-9]{8}$") + }, + new() + { + Country = "ZA", + Code = "za_vat", + Description = "South African VAT number", + Example = "4123456789", + ValidationExpression = new Regex("^[0-9]{10}$") + }, + new() + { + Country = "KR", + Code = "kr_brn", + Description = "Korean BRN", + Example = "123-45-67890", + ValidationExpression = new Regex("^[0-9]{3}-?[0-9]{2}-?[0-9]{5}$") + }, + new() + { + Country = "ES", + Code = "es_cif", + Description = "Spanish NIF/CIF number", + Example = "A12345678", + ValidationExpression = new Regex("^[A-Z]{1}[0-9]{8}$") + }, + new() + { + Country = "ES", + Code = "eu_vat", + Description = "European VAT number (Spain)", + Example = "ESA1234567Z", + ValidationExpression = new Regex("^ES[A-Z]{1}[0-9]{7}[A-Z]{1}$") + }, + new() + { + Country = "SE", + Code = "eu_vat", + Description = "European VAT number (Sweden)", + Example = "SE123456789123", + ValidationExpression = new Regex("^SE[0-9]{12}$") + }, + new() + { + Country = "CH", + Code = "ch_uid", + Description = "Switzerland UID number", + Example = "CHE-123.456.789 HR", + ValidationExpression = new Regex("^CHE-?[0-9]{3}.?[0-9]{3}.?[0-9]{3} ?HR$") + }, + new() + { + Country = "CH", + Code = "ch_vat", + Description = "Switzerland VAT number", + Example = "CHE-123.456.789 MWST", + ValidationExpression = new Regex("^CHE-?[0-9]{3}.?[0-9]{3}.?[0-9]{3} ?MWST$") + }, + new() + { + Country = "TW", + Code = "tw_vat", + Description = "Taiwanese VAT", + Example = "12345678", + ValidationExpression = new Regex("^[0-9]{8}$") + }, + new() + { + Country = "TZ", + Code = "tz_vat", + Description = "Tanzania VAT Number", + Example = "12345678A", + ValidationExpression = new Regex("^[0-9]{8}[A-Z]{1}$") + }, + new() + { + Country = "TH", + Code = "th_vat", + Description = "Thai VAT", + Example = "1234567891234", + ValidationExpression = new Regex("^[0-9]{13}$") + }, + new() + { + Country = "TR", + Code = "tr_tin", + Description = "Turkish TIN Number", + Example = "0123456789", + ValidationExpression = new Regex("^[0-9]{10}$") + }, + new() + { + Country = "UA", + Code = "ua_vat", + Description = "Ukrainian VAT", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "AE", + Code = "ae_trn", + Description = "United Arab Emirates TRN", + Example = "123456789012345", + ValidationExpression = new Regex("^[0-9]{15}$") + }, + new() + { + Country = "GB", + Code = "eu_vat", + Description = "Northern Ireland VAT number", + Example = "XI123456789", + ValidationExpression = new Regex("^XI[0-9]{9}$") + }, + new() + { + Country = "GB", + Code = "gb_vat", + Description = "United Kingdom VAT number", + Example = "GB123456789", + ValidationExpression = new Regex("^GB[0-9]{9}$") + }, + new() + { + Country = "US", + Code = "us_ein", + Description = "United States EIN", + Example = "12-3456789", + ValidationExpression = new Regex("^[0-9]{2}-?[0-9]{7}$") + }, + new() + { + Country = "UY", + Code = "uy_ruc", + Description = "Uruguayan RUC number", + Example = "123456789012", + ValidationExpression = new Regex("^[0-9]{12}$") + }, + new() + { + Country = "UZ", + Code = "uz_tin", + Description = "Uzbekistan TIN Number", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "UZ", + Code = "uz_vat", + Description = "Uzbekistan VAT Number", + Example = "123456789012", + ValidationExpression = new Regex("^[0-9]{12}$") + }, + new() + { + Country = "VE", + Code = "ve_rif", + Description = "Venezuelan RIF number", + Example = "A-12345678-9", + ValidationExpression = new Regex("^[A-Z]{1}-?[0-9]{8}-?[0-9]{1}$") + }, + new() + { + Country = "VN", + Code = "vn_tin", + Description = "Vietnamese tax ID number", + Example = "1234567890", + ValidationExpression = new Regex("^[0-9]{10}$") + } + ]; + + public string GetStripeTaxCode(string country, string taxId) + { + foreach (var taxIdType in _taxIdTypes.Where(x => x.Country == country)) + { + if (taxIdType.ValidationExpression.IsMatch(taxId)) + { + return taxIdType.Code; + } + } + + return null; + } + + public bool IsSupported(string country) + { + return _taxIdTypes.Any(x => x.Country == country); + } +} diff --git a/src/Core/Billing/Utilities.cs b/src/Core/Billing/Utilities.cs index 28527af0c0..695a3b1bb4 100644 --- a/src/Core/Billing/Utilities.cs +++ b/src/Core/Billing/Utilities.cs @@ -83,6 +83,7 @@ public static class Utilities customer.Address.Country, customer.Address.PostalCode, customer.TaxIds?.FirstOrDefault()?.Value, + customer.TaxIds?.FirstOrDefault()?.Type, customer.Address.Line1, customer.Address.Line2, customer.Address.City, diff --git a/src/Core/Models/Business/TaxInfo.cs b/src/Core/Models/Business/TaxInfo.cs index 4424576ec9..b12c5229b3 100644 --- a/src/Core/Models/Business/TaxInfo.cs +++ b/src/Core/Models/Business/TaxInfo.cs @@ -2,18 +2,9 @@ public class TaxInfo { - private string _taxIdNumber = null; - private string _taxIdType = null; + public string TaxIdNumber { get; set; } + public string TaxIdType { get; set; } - public string TaxIdNumber - { - get => _taxIdNumber; - set - { - _taxIdNumber = value; - _taxIdType = null; - } - } public string StripeTaxRateId { get; set; } public string BillingAddressLine1 { get; set; } public string BillingAddressLine2 { get; set; } @@ -21,201 +12,6 @@ public class TaxInfo public string BillingAddressState { get; set; } public string BillingAddressPostalCode { get; set; } public string BillingAddressCountry { get; set; } = "US"; - public string TaxIdType - { - get - { - if (string.IsNullOrWhiteSpace(BillingAddressCountry) || - string.IsNullOrWhiteSpace(TaxIdNumber)) - { - return null; - } - if (!string.IsNullOrWhiteSpace(_taxIdType)) - { - return _taxIdType; - } - - switch (BillingAddressCountry.ToUpper()) - { - case "AD": - _taxIdType = "ad_nrt"; - break; - case "AE": - _taxIdType = "ae_trn"; - break; - case "AR": - _taxIdType = "ar_cuit"; - break; - case "AU": - _taxIdType = "au_abn"; - break; - case "BO": - _taxIdType = "bo_tin"; - break; - case "BR": - _taxIdType = "br_cnpj"; - break; - case "CA": - // May break for those in Québec given the assumption of QST - if (BillingAddressState?.Contains("bec") ?? false) - { - _taxIdType = "ca_qst"; - break; - } - _taxIdType = "ca_bn"; - break; - case "CH": - _taxIdType = "ch_vat"; - break; - case "CL": - _taxIdType = "cl_tin"; - break; - case "CN": - _taxIdType = "cn_tin"; - break; - case "CO": - _taxIdType = "co_nit"; - break; - case "CR": - _taxIdType = "cr_tin"; - break; - case "DO": - _taxIdType = "do_rcn"; - break; - case "EC": - _taxIdType = "ec_ruc"; - break; - case "EG": - _taxIdType = "eg_tin"; - break; - case "GE": - _taxIdType = "ge_vat"; - break; - case "ID": - _taxIdType = "id_npwp"; - break; - case "IL": - _taxIdType = "il_vat"; - break; - case "IS": - _taxIdType = "is_vat"; - break; - case "KE": - _taxIdType = "ke_pin"; - break; - case "AT": - case "BE": - case "BG": - case "CY": - case "CZ": - case "DE": - case "DK": - case "EE": - case "ES": - case "FI": - case "FR": - case "GB": - case "GR": - case "HR": - case "HU": - case "IE": - case "IT": - case "LT": - case "LU": - case "LV": - case "MT": - case "NL": - case "PL": - case "PT": - case "RO": - case "SE": - case "SI": - case "SK": - _taxIdType = "eu_vat"; - break; - case "HK": - _taxIdType = "hk_br"; - break; - case "IN": - _taxIdType = "in_gst"; - break; - case "JP": - _taxIdType = "jp_cn"; - break; - case "KR": - _taxIdType = "kr_brn"; - break; - case "LI": - _taxIdType = "li_uid"; - break; - case "MX": - _taxIdType = "mx_rfc"; - break; - case "MY": - _taxIdType = "my_sst"; - break; - case "NO": - _taxIdType = "no_vat"; - break; - case "NZ": - _taxIdType = "nz_gst"; - break; - case "PE": - _taxIdType = "pe_ruc"; - break; - case "PH": - _taxIdType = "ph_tin"; - break; - case "RS": - _taxIdType = "rs_pib"; - break; - case "RU": - _taxIdType = "ru_inn"; - break; - case "SA": - _taxIdType = "sa_vat"; - break; - case "SG": - _taxIdType = "sg_gst"; - break; - case "SV": - _taxIdType = "sv_nit"; - break; - case "TH": - _taxIdType = "th_vat"; - break; - case "TR": - _taxIdType = "tr_tin"; - break; - case "TW": - _taxIdType = "tw_vat"; - break; - case "UA": - _taxIdType = "ua_vat"; - break; - case "US": - _taxIdType = "us_ein"; - break; - case "UY": - _taxIdType = "uy_ruc"; - break; - case "VE": - _taxIdType = "ve_rif"; - break; - case "VN": - _taxIdType = "vn_tin"; - break; - case "ZA": - _taxIdType = "za_vat"; - break; - default: - _taxIdType = null; - break; - } - - return _taxIdType; - } - } public bool HasTaxId { diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index bf9d047029..7d0f9d3c63 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -1,6 +1,9 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing.Models; +using Bit.Core.Billing.Models.Api.Requests.Accounts; +using Bit.Core.Billing.Models.Api.Requests.Organizations; +using Bit.Core.Billing.Models.Api.Responses; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; @@ -59,4 +62,7 @@ public interface IPaymentService Task RisksSubscriptionFailure(Organization organization); Task HasSecretsManagerStandalone(Organization organization); Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Stripe.Subscription subscription); + Task PreviewInvoiceAsync(PreviewIndividualInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId); + Task PreviewInvoiceAsync(PreviewOrganizationInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId); + } diff --git a/src/Core/Services/IStripeAdapter.cs b/src/Core/Services/IStripeAdapter.cs index 30583ef0b3..ef2e3ab766 100644 --- a/src/Core/Services/IStripeAdapter.cs +++ b/src/Core/Services/IStripeAdapter.cs @@ -31,6 +31,7 @@ public interface IStripeAdapter Task InvoiceUpcomingAsync(Stripe.UpcomingInvoiceOptions options); Task InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options); Task> InvoiceListAsync(StripeInvoiceListOptions options); + Task InvoiceCreatePreviewAsync(InvoiceCreatePreviewOptions options); Task> InvoiceSearchAsync(InvoiceSearchOptions options); Task InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options); Task InvoiceFinalizeInvoiceAsync(string id, Stripe.InvoiceFinalizeOptions options); @@ -42,6 +43,7 @@ public interface IStripeAdapter IAsyncEnumerable PaymentMethodListAutoPagingAsync(Stripe.PaymentMethodListOptions options); Task PaymentMethodAttachAsync(string id, Stripe.PaymentMethodAttachOptions options = null); Task PaymentMethodDetachAsync(string id, Stripe.PaymentMethodDetachOptions options = null); + Task PlanGetAsync(string id, Stripe.PlanGetOptions options = null); Task TaxRateCreateAsync(Stripe.TaxRateCreateOptions options); Task TaxRateUpdateAsync(string id, Stripe.TaxRateUpdateOptions options); Task TaxIdCreateAsync(string id, Stripe.TaxIdCreateOptions options); diff --git a/src/Core/Services/Implementations/StripeAdapter.cs b/src/Core/Services/Implementations/StripeAdapter.cs index 8d18331456..f4f8efe75f 100644 --- a/src/Core/Services/Implementations/StripeAdapter.cs +++ b/src/Core/Services/Implementations/StripeAdapter.cs @@ -15,6 +15,7 @@ public class StripeAdapter : IStripeAdapter private readonly Stripe.RefundService _refundService; private readonly Stripe.CardService _cardService; private readonly Stripe.BankAccountService _bankAccountService; + private readonly Stripe.PlanService _planService; private readonly Stripe.PriceService _priceService; private readonly Stripe.SetupIntentService _setupIntentService; private readonly Stripe.TestHelpers.TestClockService _testClockService; @@ -33,6 +34,7 @@ public class StripeAdapter : IStripeAdapter _cardService = new Stripe.CardService(); _bankAccountService = new Stripe.BankAccountService(); _priceService = new Stripe.PriceService(); + _planService = new Stripe.PlanService(); _setupIntentService = new SetupIntentService(); _testClockService = new Stripe.TestHelpers.TestClockService(); _customerBalanceTransactionService = new CustomerBalanceTransactionService(); @@ -133,6 +135,11 @@ public class StripeAdapter : IStripeAdapter return invoices; } + public Task InvoiceCreatePreviewAsync(InvoiceCreatePreviewOptions options) + { + return _invoiceService.CreatePreviewAsync(options); + } + public async Task> InvoiceSearchAsync(InvoiceSearchOptions options) => (await _invoiceService.SearchAsync(options)).Data; @@ -184,6 +191,11 @@ public class StripeAdapter : IStripeAdapter return _paymentMethodService.DetachAsync(id, options); } + public Task PlanGetAsync(string id, Stripe.PlanGetOptions options = null) + { + return _planService.GetAsync(id, options); + } + public Task TaxRateCreateAsync(Stripe.TaxRateCreateOptions options) { return _taxRateService.CreateAsync(options); diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 259a4eb757..c32dcd43ad 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -1,8 +1,13 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; +using Bit.Core.Billing.Models.Api.Requests.Accounts; +using Bit.Core.Billing.Models.Api.Requests.Organizations; +using Bit.Core.Billing.Models.Api.Responses; using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -32,6 +37,7 @@ public class StripePaymentService : IPaymentService private readonly IStripeAdapter _stripeAdapter; private readonly IGlobalSettings _globalSettings; private readonly IFeatureService _featureService; + private readonly ITaxService _taxService; public StripePaymentService( ITransactionRepository transactionRepository, @@ -40,7 +46,8 @@ public class StripePaymentService : IPaymentService IStripeAdapter stripeAdapter, Braintree.IBraintreeGateway braintreeGateway, IGlobalSettings globalSettings, - IFeatureService featureService) + IFeatureService featureService, + ITaxService taxService) { _transactionRepository = transactionRepository; _logger = logger; @@ -49,6 +56,7 @@ public class StripePaymentService : IPaymentService _btGateway = braintreeGateway; _globalSettings = globalSettings; _featureService = featureService; + _taxService = taxService; } public async Task PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, @@ -112,6 +120,20 @@ public class StripePaymentService : IPaymentService Subscription subscription; try { + if (taxInfo.TaxIdNumber != null && taxInfo.TaxIdType == null) + { + taxInfo.TaxIdType = _taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry, + taxInfo.TaxIdNumber); + + if (taxInfo.TaxIdType == null) + { + _logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", + taxInfo.BillingAddressCountry, + taxInfo.TaxIdNumber); + throw new BadRequestException("billingTaxIdTypeInferenceError"); + } + } + var customerCreateOptions = new CustomerCreateOptions { Description = org.DisplayBusinessName(), @@ -146,12 +168,9 @@ public class StripePaymentService : IPaymentService City = taxInfo?.BillingAddressCity, State = taxInfo?.BillingAddressState, }, - TaxIdData = taxInfo?.HasTaxId != true - ? null - : - [ - new CustomerTaxIdDataOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber, } - ], + TaxIdData = taxInfo.HasTaxId + ? [new CustomerTaxIdDataOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber }] + : null }; customerCreateOptions.AddExpand("tax"); @@ -1659,6 +1678,7 @@ public class StripePaymentService : IPaymentService return new TaxInfo { TaxIdNumber = taxId?.Value, + TaxIdType = taxId?.Type, BillingAddressLine1 = address?.Line1, BillingAddressLine2 = address?.Line2, BillingAddressCity = address?.City, @@ -1670,9 +1690,13 @@ public class StripePaymentService : IPaymentService public async Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo) { - if (subscriber != null && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) + if (string.IsNullOrWhiteSpace(subscriber?.GatewayCustomerId) || subscriber.IsUser()) { - var customer = await _stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, new CustomerUpdateOptions + return; + } + + var customer = await _stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, + new CustomerUpdateOptions { Address = new AddressOptions { @@ -1686,23 +1710,59 @@ public class StripePaymentService : IPaymentService Expand = ["tax_ids"] }); - if (!subscriber.IsUser() && customer != null) - { - var taxId = customer.TaxIds?.FirstOrDefault(); + if (customer == null) + { + return; + } - if (taxId != null) - { - await _stripeAdapter.TaxIdDeleteAsync(customer.Id, taxId.Id); - } - if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber) && - !string.IsNullOrWhiteSpace(taxInfo.TaxIdType)) - { - await _stripeAdapter.TaxIdCreateAsync(customer.Id, new TaxIdCreateOptions - { - Type = taxInfo.TaxIdType, - Value = taxInfo.TaxIdNumber, - }); - } + var taxId = customer.TaxIds?.FirstOrDefault(); + + if (taxId != null) + { + await _stripeAdapter.TaxIdDeleteAsync(customer.Id, taxId.Id); + } + + if (string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber)) + { + return; + } + + var taxIdType = taxInfo.TaxIdType; + + if (string.IsNullOrWhiteSpace(taxIdType)) + { + taxIdType = _taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry, taxInfo.TaxIdNumber); + + if (taxIdType == null) + { + _logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", + taxInfo.BillingAddressCountry, + taxInfo.TaxIdNumber); + throw new BadRequestException("billingTaxIdTypeInferenceError"); + } + } + + try + { + await _stripeAdapter.TaxIdCreateAsync(customer.Id, + new TaxIdCreateOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber, }); + } + catch (StripeException e) + { + switch (e.StripeError.Code) + { + case StripeConstants.ErrorCodes.TaxIdInvalid: + _logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", + taxInfo.TaxIdNumber, + taxInfo.BillingAddressCountry); + throw new BadRequestException("billingInvalidTaxIdError"); + default: + _logger.LogError(e, + "Error creating tax ID '{TaxId}' in country '{Country}' for customer '{CustomerID}'.", + taxInfo.TaxIdNumber, + taxInfo.BillingAddressCountry, + customer.Id); + throw new BadRequestException("billingTaxIdCreationError"); } } } @@ -1835,6 +1895,285 @@ public class StripePaymentService : IPaymentService } } + public async Task PreviewInvoiceAsync( + PreviewIndividualInvoiceRequestBody parameters, + string gatewayCustomerId, + string gatewaySubscriptionId) + { + var options = new InvoiceCreatePreviewOptions + { + AutomaticTax = new InvoiceAutomaticTaxOptions + { + Enabled = true, + }, + Currency = "usd", + Discounts = new List(), + SubscriptionDetails = new InvoiceSubscriptionDetailsOptions + { + Items = + [ + new() + { + Quantity = 1, + Plan = "premium-annually" + }, + + new() + { + Quantity = parameters.PasswordManager.AdditionalStorage, + Plan = "storage-gb-annually" + } + ] + }, + CustomerDetails = new InvoiceCustomerDetailsOptions + { + Address = new AddressOptions + { + PostalCode = parameters.TaxInformation.PostalCode, + Country = parameters.TaxInformation.Country, + } + }, + }; + + if (!string.IsNullOrEmpty(parameters.TaxInformation.TaxId)) + { + var taxIdType = _taxService.GetStripeTaxCode( + options.CustomerDetails.Address.Country, + parameters.TaxInformation.TaxId); + + if (taxIdType == null) + { + _logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", + parameters.TaxInformation.TaxId, + parameters.TaxInformation.Country); + throw new BadRequestException("billingPreviewInvalidTaxIdError"); + } + + options.CustomerDetails.TaxIds = [ + new InvoiceCustomerDetailsTaxIdOptions + { + Type = taxIdType, + Value = parameters.TaxInformation.TaxId + } + ]; + } + + if (gatewayCustomerId != null) + { + var gatewayCustomer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId); + + if (gatewayCustomer.Discount != null) + { + options.Discounts.Add(new InvoiceDiscountOptions + { + Discount = gatewayCustomer.Discount.Id + }); + } + + if (gatewaySubscriptionId != null) + { + var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId); + + if (gatewaySubscription?.Discount != null) + { + options.Discounts.Add(new InvoiceDiscountOptions + { + Discount = gatewaySubscription.Discount.Id + }); + } + } + } + + try + { + var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options); + + var effectiveTaxRate = invoice.Tax != null && invoice.TotalExcludingTax != null + ? invoice.Tax.Value.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor() + : 0M; + + var result = new PreviewInvoiceResponseModel( + effectiveTaxRate, + invoice.TotalExcludingTax.ToMajor() ?? 0, + invoice.Tax.ToMajor() ?? 0, + invoice.Total.ToMajor()); + return result; + } + catch (StripeException e) + { + switch (e.StripeError.Code) + { + case StripeConstants.ErrorCodes.TaxIdInvalid: + _logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", + parameters.TaxInformation.TaxId, + parameters.TaxInformation.Country); + throw new BadRequestException("billingPreviewInvalidTaxIdError"); + default: + _logger.LogError(e, "Unexpected error previewing invoice with tax ID '{TaxId}' in country '{Country}'.", + parameters.TaxInformation.TaxId, + parameters.TaxInformation.Country); + throw new BadRequestException("billingPreviewInvoiceError"); + } + } + } + + public async Task PreviewInvoiceAsync( + PreviewOrganizationInvoiceRequestBody parameters, + string gatewayCustomerId, + string gatewaySubscriptionId) + { + var plan = Utilities.StaticStore.GetPlan(parameters.PasswordManager.Plan); + + var options = new InvoiceCreatePreviewOptions + { + AutomaticTax = new InvoiceAutomaticTaxOptions + { + Enabled = true, + }, + Currency = "usd", + Discounts = new List(), + SubscriptionDetails = new InvoiceSubscriptionDetailsOptions + { + Items = + [ + new() + { + Quantity = parameters.PasswordManager.AdditionalStorage, + Plan = plan.PasswordManager.StripeStoragePlanId + } + ] + }, + CustomerDetails = new InvoiceCustomerDetailsOptions + { + Address = new AddressOptions + { + PostalCode = parameters.TaxInformation.PostalCode, + Country = parameters.TaxInformation.Country, + } + }, + }; + + if (plan.PasswordManager.HasAdditionalSeatsOption) + { + options.SubscriptionDetails.Items.Add( + new() + { + Quantity = parameters.PasswordManager.Seats, + Plan = plan.PasswordManager.StripeSeatPlanId + } + ); + } + else + { + options.SubscriptionDetails.Items.Add( + new() + { + Quantity = 1, + Plan = plan.PasswordManager.StripePlanId + } + ); + } + + if (plan.SupportsSecretsManager) + { + if (plan.SecretsManager.HasAdditionalSeatsOption) + { + options.SubscriptionDetails.Items.Add(new() + { + Quantity = parameters.SecretsManager?.Seats ?? 0, + Plan = plan.SecretsManager.StripeSeatPlanId + }); + } + + if (plan.SecretsManager.HasAdditionalServiceAccountOption) + { + options.SubscriptionDetails.Items.Add(new() + { + Quantity = parameters.SecretsManager?.AdditionalMachineAccounts ?? 0, + Plan = plan.SecretsManager.StripeServiceAccountPlanId + }); + } + } + + if (!string.IsNullOrEmpty(parameters.TaxInformation.TaxId)) + { + var taxIdType = _taxService.GetStripeTaxCode( + options.CustomerDetails.Address.Country, + parameters.TaxInformation.TaxId); + + if (taxIdType == null) + { + _logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", + parameters.TaxInformation.TaxId, + parameters.TaxInformation.Country); + throw new BadRequestException("billingTaxIdTypeInferenceError"); + } + + options.CustomerDetails.TaxIds = [ + new InvoiceCustomerDetailsTaxIdOptions + { + Type = taxIdType, + Value = parameters.TaxInformation.TaxId + } + ]; + } + + if (gatewayCustomerId != null) + { + var gatewayCustomer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId); + + if (gatewayCustomer.Discount != null) + { + options.Discounts.Add(new InvoiceDiscountOptions + { + Discount = gatewayCustomer.Discount.Id + }); + } + + var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId); + + if (gatewaySubscription?.Discount != null) + { + options.Discounts.Add(new InvoiceDiscountOptions + { + Discount = gatewaySubscription.Discount.Id + }); + } + } + + try + { + var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options); + + var effectiveTaxRate = invoice.Tax != null && invoice.TotalExcludingTax != null + ? invoice.Tax.Value.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor() + : 0M; + + var result = new PreviewInvoiceResponseModel( + effectiveTaxRate, + invoice.TotalExcludingTax.ToMajor() ?? 0, + invoice.Tax.ToMajor() ?? 0, + invoice.Total.ToMajor()); + return result; + } + catch (StripeException e) + { + switch (e.StripeError.Code) + { + case StripeConstants.ErrorCodes.TaxIdInvalid: + _logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", + parameters.TaxInformation.TaxId, + parameters.TaxInformation.Country); + throw new BadRequestException("billingPreviewInvalidTaxIdError"); + default: + _logger.LogError(e, "Unexpected error previewing invoice with tax ID '{TaxId}' in country '{Country}'.", + parameters.TaxInformation.TaxId, + parameters.TaxInformation.Country); + throw new BadRequestException("billingPreviewInvoiceError"); + } + } + } + private PaymentMethod GetLatestCardPaymentMethod(string customerId) { var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging( diff --git a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs index 385b185ffe..9c25ffdc55 100644 --- a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs +++ b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs @@ -1545,7 +1545,7 @@ public class SubscriberServiceTests { var stripeAdapter = sutProvider.GetDependency(); - var customer = new Customer { Id = provider.GatewayCustomerId, TaxIds = new StripeList { Data = [new TaxId { Id = "tax_id_1" }] } }; + var customer = new Customer { Id = provider.GatewayCustomerId, TaxIds = new StripeList { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] } }; stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId, Arg.Is( options => options.Expand.Contains("tax_ids"))).Returns(customer); @@ -1554,6 +1554,7 @@ public class SubscriberServiceTests "US", "12345", "123456789", + "us_ein", "123 Example St.", null, "Example Town", diff --git a/test/Core.Test/Models/Business/TaxInfoTests.cs b/test/Core.Test/Models/Business/TaxInfoTests.cs deleted file mode 100644 index 197948006e..0000000000 --- a/test/Core.Test/Models/Business/TaxInfoTests.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Bit.Core.Models.Business; -using Xunit; - -namespace Bit.Core.Test.Models.Business; - -public class TaxInfoTests -{ - // PH = Placeholder - [Theory] - [InlineData(null, null, null, null)] - [InlineData("", "", null, null)] - [InlineData("PH", "", null, null)] - [InlineData("", "PH", null, null)] - [InlineData("AE", "PH", null, "ae_trn")] - [InlineData("AU", "PH", null, "au_abn")] - [InlineData("BR", "PH", null, "br_cnpj")] - [InlineData("CA", "PH", "bec", "ca_qst")] - [InlineData("CA", "PH", null, "ca_bn")] - [InlineData("CL", "PH", null, "cl_tin")] - [InlineData("AT", "PH", null, "eu_vat")] - [InlineData("BE", "PH", null, "eu_vat")] - [InlineData("BG", "PH", null, "eu_vat")] - [InlineData("CY", "PH", null, "eu_vat")] - [InlineData("CZ", "PH", null, "eu_vat")] - [InlineData("DE", "PH", null, "eu_vat")] - [InlineData("DK", "PH", null, "eu_vat")] - [InlineData("EE", "PH", null, "eu_vat")] - [InlineData("ES", "PH", null, "eu_vat")] - [InlineData("FI", "PH", null, "eu_vat")] - [InlineData("FR", "PH", null, "eu_vat")] - [InlineData("GB", "PH", null, "eu_vat")] - [InlineData("GR", "PH", null, "eu_vat")] - [InlineData("HR", "PH", null, "eu_vat")] - [InlineData("HU", "PH", null, "eu_vat")] - [InlineData("IE", "PH", null, "eu_vat")] - [InlineData("IT", "PH", null, "eu_vat")] - [InlineData("LT", "PH", null, "eu_vat")] - [InlineData("LU", "PH", null, "eu_vat")] - [InlineData("LV", "PH", null, "eu_vat")] - [InlineData("MT", "PH", null, "eu_vat")] - [InlineData("NL", "PH", null, "eu_vat")] - [InlineData("PL", "PH", null, "eu_vat")] - [InlineData("PT", "PH", null, "eu_vat")] - [InlineData("RO", "PH", null, "eu_vat")] - [InlineData("SE", "PH", null, "eu_vat")] - [InlineData("SI", "PH", null, "eu_vat")] - [InlineData("SK", "PH", null, "eu_vat")] - [InlineData("HK", "PH", null, "hk_br")] - [InlineData("IN", "PH", null, "in_gst")] - [InlineData("JP", "PH", null, "jp_cn")] - [InlineData("KR", "PH", null, "kr_brn")] - [InlineData("LI", "PH", null, "li_uid")] - [InlineData("MX", "PH", null, "mx_rfc")] - [InlineData("MY", "PH", null, "my_sst")] - [InlineData("NO", "PH", null, "no_vat")] - [InlineData("NZ", "PH", null, "nz_gst")] - [InlineData("RU", "PH", null, "ru_inn")] - [InlineData("SA", "PH", null, "sa_vat")] - [InlineData("SG", "PH", null, "sg_gst")] - [InlineData("TH", "PH", null, "th_vat")] - [InlineData("TW", "PH", null, "tw_vat")] - [InlineData("US", "PH", null, "us_ein")] - [InlineData("ZA", "PH", null, "za_vat")] - [InlineData("ABCDEF", "PH", null, null)] - public void GetTaxIdType_Success(string billingAddressCountry, - string taxIdNumber, - string billingAddressState, - string expectedTaxIdType) - { - var taxInfo = new TaxInfo - { - BillingAddressCountry = billingAddressCountry, - TaxIdNumber = taxIdNumber, - BillingAddressState = billingAddressState, - }; - - Assert.Equal(expectedTaxIdType, taxInfo.TaxIdType); - } - - [Fact] - public void GetTaxIdType_CreateOnce_ReturnCacheSecondTime() - { - var taxInfo = new TaxInfo - { - BillingAddressCountry = "US", - TaxIdNumber = "PH", - BillingAddressState = null, - }; - - Assert.Equal("us_ein", taxInfo.TaxIdType); - - // Per the current spec even if the values change to something other than null it - // will return the cached version of TaxIdType. - taxInfo.BillingAddressCountry = "ZA"; - - Assert.Equal("us_ein", taxInfo.TaxIdType); - } - - [Theory] - [InlineData(null, null, false)] - [InlineData("123", "US", true)] - [InlineData("123", "ZQ12", false)] - [InlineData(" ", "US", false)] - public void HasTaxId_ReturnsExpected(string taxIdNumber, string billingAddressCountry, bool expected) - { - var taxInfo = new TaxInfo - { - TaxIdNumber = taxIdNumber, - BillingAddressCountry = billingAddressCountry, - }; - - Assert.Equal(expected, taxInfo.HasTaxId); - } -} diff --git a/test/Core.Test/Services/StripePaymentServiceTests.cs b/test/Core.Test/Services/StripePaymentServiceTests.cs index e15f07b113..35e1901a2f 100644 --- a/test/Core.Test/Services/StripePaymentServiceTests.cs +++ b/test/Core.Test/Services/StripePaymentServiceTests.cs @@ -77,7 +77,8 @@ public class StripePaymentServiceTests c.Address.Line2 == taxInfo.BillingAddressLine2 && c.Address.City == taxInfo.BillingAddressCity && c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData == null + c.TaxIdData.First().Value == taxInfo.TaxIdNumber && + c.TaxIdData.First().Type == taxInfo.TaxIdType )); await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => @@ -134,7 +135,8 @@ public class StripePaymentServiceTests c.Address.Line2 == taxInfo.BillingAddressLine2 && c.Address.City == taxInfo.BillingAddressCity && c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData == null + c.TaxIdData.First().Value == taxInfo.TaxIdNumber && + c.TaxIdData.First().Type == taxInfo.TaxIdType )); await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => @@ -190,7 +192,8 @@ public class StripePaymentServiceTests c.Address.Line2 == taxInfo.BillingAddressLine2 && c.Address.City == taxInfo.BillingAddressCity && c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData == null + c.TaxIdData.First().Value == taxInfo.TaxIdNumber && + c.TaxIdData.First().Type == taxInfo.TaxIdType )); await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => @@ -247,7 +250,8 @@ public class StripePaymentServiceTests c.Address.Line2 == taxInfo.BillingAddressLine2 && c.Address.City == taxInfo.BillingAddressCity && c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData == null + c.TaxIdData.First().Value == taxInfo.TaxIdNumber && + c.TaxIdData.First().Type == taxInfo.TaxIdType )); await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => @@ -441,7 +445,8 @@ public class StripePaymentServiceTests c.Address.Line2 == taxInfo.BillingAddressLine2 && c.Address.City == taxInfo.BillingAddressCity && c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData == null + c.TaxIdData.First().Value == taxInfo.TaxIdNumber && + c.TaxIdData.First().Type == taxInfo.TaxIdType )); await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => @@ -510,7 +515,8 @@ public class StripePaymentServiceTests c.Address.Line2 == taxInfo.BillingAddressLine2 && c.Address.City == taxInfo.BillingAddressCity && c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData == null + c.TaxIdData.First().Value == taxInfo.TaxIdNumber && + c.TaxIdData.First().Type == taxInfo.TaxIdType )); await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => From 470a12640ee9f42b725cdf4d2587a59ec4dc121b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ch=C4=99ci=C5=84ski?= Date: Wed, 4 Dec 2024 14:18:58 +0100 Subject: [PATCH 30/94] Trigger unified build on rc and hotfix-rc branches (#5108) --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1d092d8b4d..0fa03312b8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -530,7 +530,9 @@ jobs: self-host-build: name: Trigger self-host build - if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' + if: | + github.event_name != 'pull_request_target' + && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') runs-on: ubuntu-22.04 needs: - build-docker From 90a9473a5ecf4e0d2e912bfb7dd7209b310a9673 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Wed, 4 Dec 2024 15:36:11 +0100 Subject: [PATCH 31/94] Revert "[PM-13999] Show estimated tax for taxable countries (#5077)" (#5109) This reverts commit 94fdfa40e8af9c9b788aafe2cf89eacc2913eeea. Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> --- .../Billing/TaxServiceTests.cs | 151 --- .../Controllers/AccountsBillingController.cs | 15 - .../Billing/Controllers/InvoicesController.cs | 42 - .../Controllers/ProviderBillingController.cs | 1 - .../Billing/Controllers/StripeController.cs | 14 +- .../Requests/TaxInformationRequestBody.cs | 2 - .../Billing/Extensions/CurrencyExtensions.cs | 33 - .../Extensions/ServiceCollectionExtensions.cs | 1 - .../PreviewIndividualInvoiceRequestModel.cs | 18 - .../PreviewOrganizationInvoiceRequestModel.cs | 37 - .../Requests/TaxInformationRequestModel.cs | 14 - .../Responses/PreviewInvoiceResponseModel.cs | 7 - src/Core/Billing/Models/PreviewInvoiceInfo.cs | 7 - .../Billing/Models/Sales/OrganizationSale.cs | 1 - src/Core/Billing/Models/TaxIdType.cs | 22 - src/Core/Billing/Models/TaxInformation.cs | 160 +++- src/Core/Billing/Services/ITaxService.cs | 22 - .../OrganizationBillingService.cs | 35 +- .../PremiumUserBillingService.cs | 37 +- .../Implementations/SubscriberService.cs | 49 +- src/Core/Billing/Services/TaxService.cs | 901 ------------------ src/Core/Billing/Utilities.cs | 1 - src/Core/Models/Business/TaxInfo.cs | 208 +++- src/Core/Services/IPaymentService.cs | 6 - src/Core/Services/IStripeAdapter.cs | 2 - .../Services/Implementations/StripeAdapter.cs | 12 - .../Implementations/StripePaymentService.cs | 387 +------- .../Services/SubscriberServiceTests.cs | 3 +- .../Core.Test/Models/Business/TaxInfoTests.cs | 114 +++ .../Services/StripePaymentServiceTests.cs | 18 +- 30 files changed, 529 insertions(+), 1791 deletions(-) delete mode 100644 bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs delete mode 100644 src/Api/Billing/Controllers/InvoicesController.cs delete mode 100644 src/Core/Billing/Extensions/CurrencyExtensions.cs delete mode 100644 src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs delete mode 100644 src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs delete mode 100644 src/Core/Billing/Models/Api/Requests/TaxInformationRequestModel.cs delete mode 100644 src/Core/Billing/Models/Api/Responses/PreviewInvoiceResponseModel.cs delete mode 100644 src/Core/Billing/Models/PreviewInvoiceInfo.cs delete mode 100644 src/Core/Billing/Models/TaxIdType.cs delete mode 100644 src/Core/Billing/Services/ITaxService.cs delete mode 100644 src/Core/Billing/Services/TaxService.cs create mode 100644 test/Core.Test/Models/Business/TaxInfoTests.cs diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs deleted file mode 100644 index 3995fb9de6..0000000000 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs +++ /dev/null @@ -1,151 +0,0 @@ -using Bit.Core.Billing.Services; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using Xunit; - -namespace Bit.Commercial.Core.Test.Billing; - -[SutProviderCustomize] -public class TaxServiceTests -{ - [Theory] - [BitAutoData("AD", "A-123456-Z", "ad_nrt")] - [BitAutoData("AD", "A123456Z", "ad_nrt")] - [BitAutoData("AR", "20-12345678-9", "ar_cuit")] - [BitAutoData("AR", "20123456789", "ar_cuit")] - [BitAutoData("AU", "01259983598", "au_abn")] - [BitAutoData("AU", "123456789123", "au_arn")] - [BitAutoData("AT", "ATU12345678", "eu_vat")] - [BitAutoData("BH", "123456789012345", "bh_vat")] - [BitAutoData("BY", "123456789", "by_tin")] - [BitAutoData("BE", "BE0123456789", "eu_vat")] - [BitAutoData("BO", "123456789", "bo_tin")] - [BitAutoData("BR", "01.234.456/5432-10", "br_cnpj")] - [BitAutoData("BR", "01234456543210", "br_cnpj")] - [BitAutoData("BR", "123.456.789-87", "br_cpf")] - [BitAutoData("BR", "12345678987", "br_cpf")] - [BitAutoData("BG", "123456789", "bg_uic")] - [BitAutoData("BG", "BG012100705", "eu_vat")] - [BitAutoData("CA", "100728494", "ca_bn")] - [BitAutoData("CA", "123456789RT0001", "ca_gst_hst")] - [BitAutoData("CA", "PST-1234-1234", "ca_pst_bc")] - [BitAutoData("CA", "123456-7", "ca_pst_mb")] - [BitAutoData("CA", "1234567", "ca_pst_sk")] - [BitAutoData("CA", "1234567890TQ1234", "ca_qst")] - [BitAutoData("CL", "11.121.326-1", "cl_tin")] - [BitAutoData("CL", "11121326-1", "cl_tin")] - [BitAutoData("CL", "23.121.326-K", "cl_tin")] - [BitAutoData("CL", "43651326-K", "cl_tin")] - [BitAutoData("CN", "123456789012345678", "cn_tin")] - [BitAutoData("CN", "123456789012345", "cn_tin")] - [BitAutoData("CO", "123.456.789-0", "co_nit")] - [BitAutoData("CO", "1234567890", "co_nit")] - [BitAutoData("CR", "1-234-567890", "cr_tin")] - [BitAutoData("CR", "1234567890", "cr_tin")] - [BitAutoData("HR", "HR12345678912", "eu_vat")] - [BitAutoData("HR", "12345678901", "hr_oib")] - [BitAutoData("CY", "CY12345678X", "eu_vat")] - [BitAutoData("CZ", "CZ12345678", "eu_vat")] - [BitAutoData("DK", "DK12345678", "eu_vat")] - [BitAutoData("DO", "123-4567890-1", "do_rcn")] - [BitAutoData("DO", "12345678901", "do_rcn")] - [BitAutoData("EC", "1234567890001", "ec_ruc")] - [BitAutoData("EG", "123456789", "eg_tin")] - [BitAutoData("SV", "1234-567890-123-4", "sv_nit")] - [BitAutoData("SV", "12345678901234", "sv_nit")] - [BitAutoData("EE", "EE123456789", "eu_vat")] - [BitAutoData("EU", "EU123456789", "eu_oss_vat")] - [BitAutoData("FI", "FI12345678", "eu_vat")] - [BitAutoData("FR", "FR12345678901", "eu_vat")] - [BitAutoData("GE", "123456789", "ge_vat")] - [BitAutoData("DE", "1234567890", "de_stn")] - [BitAutoData("DE", "DE123456789", "eu_vat")] - [BitAutoData("GR", "EL123456789", "eu_vat")] - [BitAutoData("HK", "12345678", "hk_br")] - [BitAutoData("HU", "HU12345678", "eu_vat")] - [BitAutoData("HU", "12345678-1-23", "hu_tin")] - [BitAutoData("HU", "12345678123", "hu_tin")] - [BitAutoData("IS", "123456", "is_vat")] - [BitAutoData("IN", "12ABCDE1234F1Z5", "in_gst")] - [BitAutoData("IN", "12ABCDE3456FGZH", "in_gst")] - [BitAutoData("ID", "012.345.678.9-012.345", "id_npwp")] - [BitAutoData("ID", "0123456789012345", "id_npwp")] - [BitAutoData("IE", "IE1234567A", "eu_vat")] - [BitAutoData("IE", "IE1234567AB", "eu_vat")] - [BitAutoData("IL", "000012345", "il_vat")] - [BitAutoData("IL", "123456789", "il_vat")] - [BitAutoData("IT", "IT12345678901", "eu_vat")] - [BitAutoData("JP", "1234567890123", "jp_cn")] - [BitAutoData("JP", "12345", "jp_rn")] - [BitAutoData("KZ", "123456789012", "kz_bin")] - [BitAutoData("KE", "P000111111A", "ke_pin")] - [BitAutoData("LV", "LV12345678912", "eu_vat")] - [BitAutoData("LI", "CHE123456789", "li_uid")] - [BitAutoData("LI", "12345", "li_vat")] - [BitAutoData("LT", "LT123456789123", "eu_vat")] - [BitAutoData("LU", "LU12345678", "eu_vat")] - [BitAutoData("MY", "12345678", "my_frp")] - [BitAutoData("MY", "C 1234567890", "my_itn")] - [BitAutoData("MY", "C1234567890", "my_itn")] - [BitAutoData("MY", "A12-3456-78912345", "my_sst")] - [BitAutoData("MY", "A12345678912345", "my_sst")] - [BitAutoData("MT", "MT12345678", "eu_vat")] - [BitAutoData("MX", "ABC010203AB9", "mx_rfc")] - [BitAutoData("MD", "1003600", "md_vat")] - [BitAutoData("MA", "12345678", "ma_vat")] - [BitAutoData("NL", "NL123456789B12", "eu_vat")] - [BitAutoData("NZ", "123456789", "nz_gst")] - [BitAutoData("NG", "12345678-0001", "ng_tin")] - [BitAutoData("NO", "123456789MVA", "no_vat")] - [BitAutoData("NO", "1234567", "no_voec")] - [BitAutoData("OM", "OM1234567890", "om_vat")] - [BitAutoData("PE", "12345678901", "pe_ruc")] - [BitAutoData("PH", "123456789012", "ph_tin")] - [BitAutoData("PL", "PL1234567890", "eu_vat")] - [BitAutoData("PT", "PT123456789", "eu_vat")] - [BitAutoData("RO", "RO1234567891", "eu_vat")] - [BitAutoData("RO", "1234567890123", "ro_tin")] - [BitAutoData("RU", "1234567891", "ru_inn")] - [BitAutoData("RU", "123456789", "ru_kpp")] - [BitAutoData("SA", "123456789012345", "sa_vat")] - [BitAutoData("RS", "123456789", "rs_pib")] - [BitAutoData("SG", "M12345678X", "sg_gst")] - [BitAutoData("SG", "123456789F", "sg_uen")] - [BitAutoData("SK", "SK1234567891", "eu_vat")] - [BitAutoData("SI", "SI12345678", "eu_vat")] - [BitAutoData("SI", "12345678", "si_tin")] - [BitAutoData("ZA", "4123456789", "za_vat")] - [BitAutoData("KR", "123-45-67890", "kr_brn")] - [BitAutoData("KR", "1234567890", "kr_brn")] - [BitAutoData("ES", "A12345678", "es_cif")] - [BitAutoData("ES", "ESX1234567X", "eu_vat")] - [BitAutoData("SE", "SE123456789012", "eu_vat")] - [BitAutoData("CH", "CHE-123.456.789 HR", "ch_uid")] - [BitAutoData("CH", "CHE123456789HR", "ch_uid")] - [BitAutoData("CH", "CHE-123.456.789 MWST", "ch_vat")] - [BitAutoData("CH", "CHE123456789MWST", "ch_vat")] - [BitAutoData("TW", "12345678", "tw_vat")] - [BitAutoData("TH", "1234567890123", "th_vat")] - [BitAutoData("TR", "0123456789", "tr_tin")] - [BitAutoData("UA", "123456789", "ua_vat")] - [BitAutoData("AE", "123456789012345", "ae_trn")] - [BitAutoData("GB", "XI123456789", "eu_vat")] - [BitAutoData("GB", "GB123456789", "gb_vat")] - [BitAutoData("US", "12-3456789", "us_ein")] - [BitAutoData("UY", "123456789012", "uy_ruc")] - [BitAutoData("UZ", "123456789", "uz_tin")] - [BitAutoData("UZ", "123456789012", "uz_vat")] - [BitAutoData("VE", "A-12345678-9", "ve_rif")] - [BitAutoData("VE", "A123456789", "ve_rif")] - [BitAutoData("VN", "1234567890", "vn_tin")] - public void GetStripeTaxCode_WithValidCountryAndTaxId_ReturnsExpectedTaxIdType( - string country, - string taxId, - string expected, - SutProvider sutProvider) - { - var result = sutProvider.Sut.GetStripeTaxCode(country, taxId); - - Assert.Equal(expected, result); - } -} diff --git a/src/Api/Billing/Controllers/AccountsBillingController.cs b/src/Api/Billing/Controllers/AccountsBillingController.cs index fcb89226e7..574ac3e65e 100644 --- a/src/Api/Billing/Controllers/AccountsBillingController.cs +++ b/src/Api/Billing/Controllers/AccountsBillingController.cs @@ -1,6 +1,5 @@ #nullable enable using Bit.Api.Billing.Models.Responses; -using Bit.Core.Billing.Models.Api.Requests.Accounts; using Bit.Core.Billing.Services; using Bit.Core.Services; using Bit.Core.Utilities; @@ -78,18 +77,4 @@ public class AccountsBillingController( return TypedResults.Ok(transactions); } - - [HttpPost("preview-invoice")] - public async Task PreviewInvoiceAsync([FromBody] PreviewIndividualInvoiceRequestBody model) - { - var user = await userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - var invoice = await paymentService.PreviewInvoiceAsync(model, user.GatewayCustomerId, user.GatewaySubscriptionId); - - return TypedResults.Ok(invoice); - } } diff --git a/src/Api/Billing/Controllers/InvoicesController.cs b/src/Api/Billing/Controllers/InvoicesController.cs deleted file mode 100644 index 686d9b9643..0000000000 --- a/src/Api/Billing/Controllers/InvoicesController.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Models.Api.Requests.Organizations; -using Bit.Core.Context; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace Bit.Api.Billing.Controllers; - -[Route("invoices")] -[Authorize("Application")] -public class InvoicesController : BaseBillingController -{ - [HttpPost("preview-organization")] - public async Task PreviewInvoiceAsync( - [FromBody] PreviewOrganizationInvoiceRequestBody model, - [FromServices] ICurrentContext currentContext, - [FromServices] IOrganizationRepository organizationRepository, - [FromServices] IPaymentService paymentService) - { - Organization organization = null; - if (model.OrganizationId != default) - { - if (!await currentContext.EditPaymentMethods(model.OrganizationId)) - { - return Error.Unauthorized(); - } - - organization = await organizationRepository.GetByIdAsync(model.OrganizationId); - if (organization == null) - { - return Error.NotFound(); - } - } - - var invoice = await paymentService.PreviewInvoiceAsync(model, organization?.GatewayCustomerId, - organization?.GatewaySubscriptionId); - - return TypedResults.Ok(invoice); - } -} diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index c5de63c69b..f7ddf0853e 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -119,7 +119,6 @@ public class ProviderBillingController( requestBody.Country, requestBody.PostalCode, requestBody.TaxId, - requestBody.TaxIdType, requestBody.Line1, requestBody.Line2, requestBody.City, diff --git a/src/Api/Billing/Controllers/StripeController.cs b/src/Api/Billing/Controllers/StripeController.cs index f5e8253bfa..a4a974bb99 100644 --- a/src/Api/Billing/Controllers/StripeController.cs +++ b/src/Api/Billing/Controllers/StripeController.cs @@ -1,5 +1,4 @@ -using Bit.Core.Billing.Services; -using Bit.Core.Services; +using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -47,15 +46,4 @@ public class StripeController( return TypedResults.Ok(setupIntent.ClientSecret); } - - [HttpGet] - [Route("~/tax/is-country-supported")] - public IResult IsCountrySupported( - [FromQuery] string country, - [FromServices] ITaxService taxService) - { - var isSupported = taxService.IsSupported(country); - - return TypedResults.Ok(isSupported); - } } diff --git a/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs b/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs index 32ba2effb2..c5c0fde00b 100644 --- a/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs +++ b/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs @@ -10,7 +10,6 @@ public class TaxInformationRequestBody [Required] public string PostalCode { get; set; } public string TaxId { get; set; } - public string TaxIdType { get; set; } public string Line1 { get; set; } public string Line2 { get; set; } public string City { get; set; } @@ -20,7 +19,6 @@ public class TaxInformationRequestBody Country, PostalCode, TaxId, - TaxIdType, Line1, Line2, City, diff --git a/src/Core/Billing/Extensions/CurrencyExtensions.cs b/src/Core/Billing/Extensions/CurrencyExtensions.cs deleted file mode 100644 index cde1a7bea8..0000000000 --- a/src/Core/Billing/Extensions/CurrencyExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Bit.Core.Billing.Extensions; - -public static class CurrencyExtensions -{ - /// - /// Converts a currency amount in major units to minor units. - /// - /// 123.99 USD returns 12399 in minor units. - public static long ToMinor(this decimal amount) - { - return Convert.ToInt64(amount * 100); - } - - /// - /// Converts a currency amount in minor units to major units. - /// - /// - /// 12399 in minor units returns 123.99 USD. - public static decimal? ToMajor(this long? amount) - { - return amount?.ToMajor(); - } - - /// - /// Converts a currency amount in minor units to major units. - /// - /// - /// 12399 in minor units returns 123.99 USD. - public static decimal ToMajor(this long amount) - { - return Convert.ToDecimal(amount) / 100; - } -} diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index 8f2803d920..abfceac736 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -11,7 +11,6 @@ public static class ServiceCollectionExtensions { public static void AddBillingOperations(this IServiceCollection services) { - services.AddSingleton(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs b/src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs deleted file mode 100644 index 6dfb9894d5..0000000000 --- a/src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Bit.Core.Billing.Models.Api.Requests.Accounts; - -public class PreviewIndividualInvoiceRequestBody -{ - [Required] - public PasswordManagerRequestModel PasswordManager { get; set; } - - [Required] - public TaxInformationRequestModel TaxInformation { get; set; } -} - -public class PasswordManagerRequestModel -{ - [Range(0, int.MaxValue)] - public int AdditionalStorage { get; set; } -} diff --git a/src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs b/src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs deleted file mode 100644 index 18d9c352d7..0000000000 --- a/src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Bit.Core.Billing.Enums; - -namespace Bit.Core.Billing.Models.Api.Requests.Organizations; - -public class PreviewOrganizationInvoiceRequestBody -{ - public Guid OrganizationId { get; set; } - - [Required] - public PasswordManagerRequestModel PasswordManager { get; set; } - - public SecretsManagerRequestModel SecretsManager { get; set; } - - [Required] - public TaxInformationRequestModel TaxInformation { get; set; } -} - -public class PasswordManagerRequestModel -{ - public PlanType Plan { get; set; } - - [Range(0, int.MaxValue)] - public int Seats { get; set; } - - [Range(0, int.MaxValue)] - public int AdditionalStorage { get; set; } -} - -public class SecretsManagerRequestModel -{ - [Range(0, int.MaxValue)] - public int Seats { get; set; } - - [Range(0, int.MaxValue)] - public int AdditionalMachineAccounts { get; set; } -} diff --git a/src/Core/Billing/Models/Api/Requests/TaxInformationRequestModel.cs b/src/Core/Billing/Models/Api/Requests/TaxInformationRequestModel.cs deleted file mode 100644 index 9cb43645c6..0000000000 --- a/src/Core/Billing/Models/Api/Requests/TaxInformationRequestModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Bit.Core.Billing.Models.Api.Requests; - -public class TaxInformationRequestModel -{ - [Length(2, 2), Required] - public string Country { get; set; } - - [Required] - public string PostalCode { get; set; } - - public string TaxId { get; set; } -} diff --git a/src/Core/Billing/Models/Api/Responses/PreviewInvoiceResponseModel.cs b/src/Core/Billing/Models/Api/Responses/PreviewInvoiceResponseModel.cs deleted file mode 100644 index fdde7dae1e..0000000000 --- a/src/Core/Billing/Models/Api/Responses/PreviewInvoiceResponseModel.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Bit.Core.Billing.Models.Api.Responses; - -public record PreviewInvoiceResponseModel( - decimal EffectiveTaxRate, - decimal TaxableBaseAmount, - decimal TaxAmount, - decimal TotalAmount); diff --git a/src/Core/Billing/Models/PreviewInvoiceInfo.cs b/src/Core/Billing/Models/PreviewInvoiceInfo.cs deleted file mode 100644 index 16a2019c20..0000000000 --- a/src/Core/Billing/Models/PreviewInvoiceInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Bit.Core.Billing.Models; - -public record PreviewInvoiceInfo( - decimal EffectiveTaxRate, - decimal TaxableBaseAmount, - decimal TaxAmount, - decimal TotalAmount); diff --git a/src/Core/Billing/Models/Sales/OrganizationSale.cs b/src/Core/Billing/Models/Sales/OrganizationSale.cs index 43852bb320..a19c278c68 100644 --- a/src/Core/Billing/Models/Sales/OrganizationSale.cs +++ b/src/Core/Billing/Models/Sales/OrganizationSale.cs @@ -65,7 +65,6 @@ public class OrganizationSale signup.TaxInfo.BillingAddressCountry, signup.TaxInfo.BillingAddressPostalCode, signup.TaxInfo.TaxIdNumber, - signup.TaxInfo.TaxIdType, signup.TaxInfo.BillingAddressLine1, signup.TaxInfo.BillingAddressLine2, signup.TaxInfo.BillingAddressCity, diff --git a/src/Core/Billing/Models/TaxIdType.cs b/src/Core/Billing/Models/TaxIdType.cs deleted file mode 100644 index 3fc246d68b..0000000000 --- a/src/Core/Billing/Models/TaxIdType.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.RegularExpressions; - -namespace Bit.Core.Billing.Models; - -public class TaxIdType -{ - /// - /// ISO-3166-2 code for the country. - /// - public string Country { get; set; } - - /// - /// The identifier in Stripe for the tax ID type. - /// - public string Code { get; set; } - - public Regex ValidationExpression { get; set; } - - public string Description { get; set; } - - public string Example { get; set; } -} diff --git a/src/Core/Billing/Models/TaxInformation.cs b/src/Core/Billing/Models/TaxInformation.cs index 23ed3e5faa..5403f94690 100644 --- a/src/Core/Billing/Models/TaxInformation.cs +++ b/src/Core/Billing/Models/TaxInformation.cs @@ -1,4 +1,5 @@ using Bit.Core.Models.Business; +using Stripe; namespace Bit.Core.Billing.Models; @@ -6,7 +7,6 @@ public record TaxInformation( string Country, string PostalCode, string TaxId, - string TaxIdType, string Line1, string Line2, string City, @@ -16,9 +16,165 @@ public record TaxInformation( taxInfo.BillingAddressCountry, taxInfo.BillingAddressPostalCode, taxInfo.TaxIdNumber, - taxInfo.TaxIdType, taxInfo.BillingAddressLine1, taxInfo.BillingAddressLine2, taxInfo.BillingAddressCity, taxInfo.BillingAddressState); + + public (AddressOptions, List) GetStripeOptions() + { + var address = new AddressOptions + { + Country = Country, + PostalCode = PostalCode, + Line1 = Line1, + Line2 = Line2, + City = City, + State = State + }; + + var customerTaxIdDataOptionsList = !string.IsNullOrEmpty(TaxId) + ? new List { new() { Type = GetTaxIdType(), Value = TaxId } } + : null; + + return (address, customerTaxIdDataOptionsList); + } + + public string GetTaxIdType() + { + if (string.IsNullOrEmpty(Country) || string.IsNullOrEmpty(TaxId)) + { + return null; + } + + switch (Country.ToUpper()) + { + case "AD": + return "ad_nrt"; + case "AE": + return "ae_trn"; + case "AR": + return "ar_cuit"; + case "AU": + return "au_abn"; + case "BO": + return "bo_tin"; + case "BR": + return "br_cnpj"; + case "CA": + // May break for those in Québec given the assumption of QST + if (State?.Contains("bec") ?? false) + { + return "ca_qst"; + } + return "ca_bn"; + case "CH": + return "ch_vat"; + case "CL": + return "cl_tin"; + case "CN": + return "cn_tin"; + case "CO": + return "co_nit"; + case "CR": + return "cr_tin"; + case "DO": + return "do_rcn"; + case "EC": + return "ec_ruc"; + case "EG": + return "eg_tin"; + case "GE": + return "ge_vat"; + case "ID": + return "id_npwp"; + case "IL": + return "il_vat"; + case "IS": + return "is_vat"; + case "KE": + return "ke_pin"; + case "AT": + case "BE": + case "BG": + case "CY": + case "CZ": + case "DE": + case "DK": + case "EE": + case "ES": + case "FI": + case "FR": + case "GB": + case "GR": + case "HR": + case "HU": + case "IE": + case "IT": + case "LT": + case "LU": + case "LV": + case "MT": + case "NL": + case "PL": + case "PT": + case "RO": + case "SE": + case "SI": + case "SK": + return "eu_vat"; + case "HK": + return "hk_br"; + case "IN": + return "in_gst"; + case "JP": + return "jp_cn"; + case "KR": + return "kr_brn"; + case "LI": + return "li_uid"; + case "MX": + return "mx_rfc"; + case "MY": + return "my_sst"; + case "NO": + return "no_vat"; + case "NZ": + return "nz_gst"; + case "PE": + return "pe_ruc"; + case "PH": + return "ph_tin"; + case "RS": + return "rs_pib"; + case "RU": + return "ru_inn"; + case "SA": + return "sa_vat"; + case "SG": + return "sg_gst"; + case "SV": + return "sv_nit"; + case "TH": + return "th_vat"; + case "TR": + return "tr_tin"; + case "TW": + return "tw_vat"; + case "UA": + return "ua_vat"; + case "US": + return "us_ein"; + case "UY": + return "uy_ruc"; + case "VE": + return "ve_rif"; + case "VN": + return "vn_tin"; + case "ZA": + return "za_vat"; + default: + return null; + } + } } diff --git a/src/Core/Billing/Services/ITaxService.cs b/src/Core/Billing/Services/ITaxService.cs deleted file mode 100644 index beee113d17..0000000000 --- a/src/Core/Billing/Services/ITaxService.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Bit.Core.Billing.Services; - -public interface ITaxService -{ - /// - /// Retrieves the Stripe tax code for a given country and tax ID. - /// - /// - /// - /// - /// Returns the Stripe tax code if the tax ID is valid for the country. - /// Returns null if the tax ID is invalid or the country is not supported. - /// - string GetStripeTaxCode(string country, string taxId); - - /// - /// Returns true or false whether charging or storing tax is supported for the given country. - /// - /// - /// - bool IsSupported(string country); -} diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index b186a99d93..eadc589625 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -28,8 +28,7 @@ public class OrganizationBillingService( IOrganizationRepository organizationRepository, ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, - ISubscriberService subscriberService, - ITaxService taxService) : IOrganizationBillingService + ISubscriberService subscriberService) : IOrganizationBillingService { public async Task Finalize(OrganizationSale sale) { @@ -168,38 +167,14 @@ public class OrganizationBillingService( throw new BillingException(); } - customerCreateOptions.Address = new AddressOptions - { - Line1 = customerSetup.TaxInformation.Line1, - Line2 = customerSetup.TaxInformation.Line2, - City = customerSetup.TaxInformation.City, - PostalCode = customerSetup.TaxInformation.PostalCode, - State = customerSetup.TaxInformation.State, - Country = customerSetup.TaxInformation.Country, - }; + var (address, taxIdData) = customerSetup.TaxInformation.GetStripeOptions(); + + customerCreateOptions.Address = address; customerCreateOptions.Tax = new CustomerTaxOptions { ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately }; - - if (!string.IsNullOrEmpty(customerSetup.TaxInformation.TaxId)) - { - var taxIdType = taxService.GetStripeTaxCode(customerSetup.TaxInformation.Country, - customerSetup.TaxInformation.TaxId); - - if (taxIdType == null) - { - logger.LogWarning("Could not determine tax ID type for organization '{OrganizationID}' in country '{Country}' with tax ID '{TaxID}'.", - organization.Id, - customerSetup.TaxInformation.Country, - customerSetup.TaxInformation.TaxId); - } - - customerCreateOptions.TaxIdData = - [ - new() { Type = taxIdType, Value = customerSetup.TaxInformation.TaxId } - ]; - } + customerCreateOptions.TaxIdData = taxIdData; var (paymentMethodType, paymentMethodToken) = customerSetup.TokenizedPaymentSource; diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index 5351888ad9..92c81dae1c 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -24,8 +24,7 @@ public class PremiumUserBillingService( ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, ISubscriberService subscriberService, - IUserRepository userRepository, - ITaxService taxService) : IPremiumUserBillingService + IUserRepository userRepository) : IPremiumUserBillingService { public async Task Finalize(PremiumUserSale sale) { @@ -83,19 +82,13 @@ public class PremiumUserBillingService( throw new BillingException(); } + var (address, taxIdData) = customerSetup.TaxInformation.GetStripeOptions(); + var subscriberName = user.SubscriberName(); var customerCreateOptions = new CustomerCreateOptions { - Address = new AddressOptions - { - Line1 = customerSetup.TaxInformation.Line1, - Line2 = customerSetup.TaxInformation.Line2, - City = customerSetup.TaxInformation.City, - PostalCode = customerSetup.TaxInformation.PostalCode, - State = customerSetup.TaxInformation.State, - Country = customerSetup.TaxInformation.Country, - }, + Address = address, Description = user.Name, Email = user.Email, Expand = ["tax"], @@ -120,28 +113,10 @@ public class PremiumUserBillingService( Tax = new CustomerTaxOptions { ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately - } + }, + TaxIdData = taxIdData }; - if (!string.IsNullOrEmpty(customerSetup.TaxInformation.TaxId)) - { - var taxIdType = taxService.GetStripeTaxCode(customerSetup.TaxInformation.Country, - customerSetup.TaxInformation.TaxId); - - if (taxIdType == null) - { - logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", - customerSetup.TaxInformation.Country, - customerSetup.TaxInformation.TaxId); - throw new Exceptions.BadRequestException("billingTaxIdTypeInferenceError"); - } - - customerCreateOptions.TaxIdData = - [ - new() { Type = taxIdType, Value = customerSetup.TaxInformation.TaxId } - ]; - } - var (paymentMethodType, paymentMethodToken) = customerSetup.TokenizedPaymentSource; var braintreeCustomerId = ""; diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index 6125d15419..9b8f64be82 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -23,8 +23,7 @@ public class SubscriberService( IGlobalSettings globalSettings, ILogger logger, ISetupIntentCache setupIntentCache, - IStripeAdapter stripeAdapter, - ITaxService taxService) : ISubscriberService + IStripeAdapter stripeAdapter) : ISubscriberService { public async Task CancelSubscription( ISubscriber subscriber, @@ -619,47 +618,16 @@ public class SubscriberService( await stripeAdapter.TaxIdDeleteAsync(customer.Id, taxId.Id); } - if (string.IsNullOrWhiteSpace(taxInformation.TaxId)) - { - return; - } + var taxIdType = taxInformation.GetTaxIdType(); - var taxIdType = taxInformation.TaxIdType; - if (string.IsNullOrWhiteSpace(taxIdType)) + if (!string.IsNullOrWhiteSpace(taxInformation.TaxId) && + !string.IsNullOrWhiteSpace(taxIdType)) { - taxIdType = taxService.GetStripeTaxCode(taxInformation.Country, - taxInformation.TaxId); - - if (taxIdType == null) + await stripeAdapter.TaxIdCreateAsync(customer.Id, new TaxIdCreateOptions { - logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", - taxInformation.Country, - taxInformation.TaxId); - throw new Exceptions.BadRequestException("billingTaxIdTypeInferenceError"); - } - } - - try - { - await stripeAdapter.TaxIdCreateAsync(customer.Id, - new TaxIdCreateOptions { Type = taxIdType, Value = taxInformation.TaxId }); - } - catch (StripeException e) - { - switch (e.StripeError.Code) - { - case StripeConstants.ErrorCodes.TaxIdInvalid: - logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", - taxInformation.TaxId, - taxInformation.Country); - throw new Exceptions.BadRequestException("billingInvalidTaxIdError"); - default: - logger.LogError(e, "Error creating tax ID '{TaxId}' in country '{Country}' for customer '{CustomerID}'.", - taxInformation.TaxId, - taxInformation.Country, - customer.Id); - throw new Exceptions.BadRequestException("billingTaxIdCreationError"); - } + Type = taxIdType, + Value = taxInformation.TaxId, + }); } } @@ -802,7 +770,6 @@ public class SubscriberService( customer.Address.Country, customer.Address.PostalCode, customer.TaxIds?.FirstOrDefault()?.Value, - customer.TaxIds?.FirstOrDefault()?.Type, customer.Address.Line1, customer.Address.Line2, customer.Address.City, diff --git a/src/Core/Billing/Services/TaxService.cs b/src/Core/Billing/Services/TaxService.cs deleted file mode 100644 index 3066be92d1..0000000000 --- a/src/Core/Billing/Services/TaxService.cs +++ /dev/null @@ -1,901 +0,0 @@ -using System.Text.RegularExpressions; -using Bit.Core.Billing.Models; - -namespace Bit.Core.Billing.Services; - -public class TaxService : ITaxService -{ - /// - /// Retrieves a list of supported tax ID types for customers. - /// - /// Compiled list from Stripe - private static readonly IEnumerable _taxIdTypes = - [ - new() - { - Country = "AD", - Code = "ad_nrt", - Description = "Andorran NRT number", - Example = "A-123456-Z", - ValidationExpression = new Regex("^([A-Z]{1})-?([0-9]{6})-?([A-Z]{1})$") - }, - new() - { - Country = "AR", - Code = "ar_cuit", - Description = "Argentinian tax ID number", - Example = "12-34567890-1", - ValidationExpression = new Regex("^([0-9]{2})-?([0-9]{8})-?([0-9]{1})$") - }, - new() - { - Country = "AU", - Code = "au_abn", - Description = "Australian Business Number (AU ABN)", - Example = "123456789012", - ValidationExpression = new Regex("^[0-9]{11}$") - }, - new() - { - Country = "AU", - Code = "au_arn", - Description = "Australian Taxation Office Reference Number", - Example = "123456789123", - ValidationExpression = new Regex("^[0-9]{12}$") - }, - new() - { - Country = "AT", - Code = "eu_vat", - Description = "European VAT number (Austria)", - Example = "ATU12345678", - ValidationExpression = new Regex("^ATU[0-9]{8}$") - }, - new() - { - Country = "BH", - Code = "bh_vat", - Description = "Bahraini VAT Number", - Example = "123456789012345", - ValidationExpression = new Regex("^[0-9]{15}$") - }, - new() - { - Country = "BY", - Code = "by_tin", - Description = "Belarus TIN Number", - Example = "123456789", - ValidationExpression = new Regex("^[0-9]{9}$") - }, - new() - { - Country = "BE", - Code = "eu_vat", - Description = "European VAT number (Belgium)", - Example = "BE0123456789", - ValidationExpression = new Regex("^BE[0-9]{10}$") - }, - new() - { - Country = "BO", - Code = "bo_tin", - Description = "Bolivian tax ID", - Example = "123456789", - ValidationExpression = new Regex("^[0-9]{9}$") - }, - new() - { - Country = "BR", - Code = "br_cnpj", - Description = "Brazilian CNPJ number", - Example = "01.234.456/5432-10", - ValidationExpression = new Regex("^[0-9]{2}.?[0-9]{3}.?[0-9]{3}/?[0-9]{4}-?[0-9]{2}$") - }, - new() - { - Country = "BR", - Code = "br_cpf", - Description = "Brazilian CPF number", - Example = "123.456.789-87", - ValidationExpression = new Regex("^[0-9]{3}.?[0-9]{3}.?[0-9]{3}-?[0-9]{2}$") - }, - new() - { - Country = "BG", - Code = "bg_uic", - Description = "Bulgaria Unified Identification Code", - Example = "123456789", - ValidationExpression = new Regex("^[0-9]{9}$") - }, - new() - { - Country = "BG", - Code = "eu_vat", - Description = "European VAT number (Bulgaria)", - Example = "BG0123456789", - ValidationExpression = new Regex("^BG[0-9]{9,10}$") - }, - new() - { - Country = "CA", - Code = "ca_bn", - Description = "Canadian BN", - Example = "123456789", - ValidationExpression = new Regex("^[0-9]{9}$") - }, - new() - { - Country = "CA", - Code = "ca_gst_hst", - Description = "Canadian GST/HST number", - Example = "123456789RT0002", - ValidationExpression = new Regex("^[0-9]{9}RT[0-9]{4}$") - }, - new() - { - Country = "CA", - Code = "ca_pst_bc", - Description = "Canadian PST number (British Columbia)", - Example = "PST-1234-5678", - ValidationExpression = new Regex("^PST-[0-9]{4}-[0-9]{4}$") - }, - new() - { - Country = "CA", - Code = "ca_pst_mb", - Description = "Canadian PST number (Manitoba)", - Example = "123456-7", - ValidationExpression = new Regex("^[0-9]{6}-[0-9]{1}$") - }, - new() - { - Country = "CA", - Code = "ca_pst_sk", - Description = "Canadian PST number (Saskatchewan)", - Example = "1234567", - ValidationExpression = new Regex("^[0-9]{7}$") - }, - new() - { - Country = "CA", - Code = "ca_qst", - Description = "Canadian QST number (Québec)", - Example = "1234567890TQ1234", - ValidationExpression = new Regex("^[0-9]{10}TQ[0-9]{4}$") - }, - new() - { - Country = "CL", - Code = "cl_tin", - Description = "Chilean TIN", - Example = "12.345.678-K", - ValidationExpression = new Regex("^[0-9]{2}.?[0-9]{3}.?[0-9]{3}-?[0-9A-Z]{1}$") - }, - new() - { - Country = "CN", - Code = "cn_tin", - Description = "Chinese tax ID", - Example = "123456789012345678", - ValidationExpression = new Regex("^[0-9]{15,18}$") - }, - new() - { - Country = "CO", - Code = "co_nit", - Description = "Colombian NIT number", - Example = "123.456.789-0", - ValidationExpression = new Regex("^[0-9]{3}.?[0-9]{3}.?[0-9]{3}-?[0-9]{1}$") - }, - new() - { - Country = "CR", - Code = "cr_tin", - Description = "Costa Rican tax ID", - Example = "1-234-567890", - ValidationExpression = new Regex("^[0-9]{1}-?[0-9]{3}-?[0-9]{6}$") - }, - new() - { - Country = "HR", - Code = "eu_vat", - Description = "European VAT number (Croatia)", - Example = "HR12345678912", - ValidationExpression = new Regex("^HR[0-9]{11}$") - }, - new() - { - Country = "HR", - Code = "hr_oib", - Description = "Croatian Personal Identification Number", - Example = "12345678901", - ValidationExpression = new Regex("^[0-9]{11}$") - }, - new() - { - Country = "CY", - Code = "eu_vat", - Description = "European VAT number (Cyprus)", - Example = "CY12345678X", - ValidationExpression = new Regex("^CY[0-9]{8}[A-Z]{1}$") - }, - new() - { - Country = "CZ", - Code = "eu_vat", - Description = "European VAT number (Czech Republic)", - Example = "CZ12345678", - ValidationExpression = new Regex("^CZ[0-9]{8,10}$") - }, - new() - { - Country = "DK", - Code = "eu_vat", - Description = "European VAT number (Denmark)", - Example = "DK12345678", - ValidationExpression = new Regex("^DK[0-9]{8}$") - }, - new() - { - Country = "DO", - Code = "do_rcn", - Description = "Dominican RCN number", - Example = "123-4567890-1", - ValidationExpression = new Regex("^[0-9]{3}-?[0-9]{7}-?[0-9]{1}$") - }, - new() - { - Country = "EC", - Code = "ec_ruc", - Description = "Ecuadorian RUC number", - Example = "1234567890001", - ValidationExpression = new Regex("^[0-9]{13}$") - }, - new() - { - Country = "EG", - Code = "eg_tin", - Description = "Egyptian Tax Identification Number", - Example = "123456789", - ValidationExpression = new Regex("^[0-9]{9}$") - }, - - new() - { - Country = "SV", - Code = "sv_nit", - Description = "El Salvadorian NIT number", - Example = "1234-567890-123-4", - ValidationExpression = new Regex("^[0-9]{4}-?[0-9]{6}-?[0-9]{3}-?[0-9]{1}$") - }, - - new() - { - Country = "EE", - Code = "eu_vat", - Description = "European VAT number (Estonia)", - Example = "EE123456789", - ValidationExpression = new Regex("^EE[0-9]{9}$") - }, - - new() - { - Country = "EU", - Code = "eu_oss_vat", - Description = "European One Stop Shop VAT number for non-Union scheme", - Example = "EU123456789", - ValidationExpression = new Regex("^EU[0-9]{9}$") - }, - new() - { - Country = "FI", - Code = "eu_vat", - Description = "European VAT number (Finland)", - Example = "FI12345678", - ValidationExpression = new Regex("^FI[0-9]{8}$") - }, - new() - { - Country = "FR", - Code = "eu_vat", - Description = "European VAT number (France)", - Example = "FR12345678901", - ValidationExpression = new Regex("^FR[0-9A-Z]{2}[0-9]{9}$") - }, - new() - { - Country = "GE", - Code = "ge_vat", - Description = "Georgian VAT", - Example = "123456789", - ValidationExpression = new Regex("^[0-9]{9}$") - }, - new() - { - Country = "DE", - Code = "de_stn", - Description = "German Tax Number (Steuernummer)", - Example = "1234567890", - ValidationExpression = new Regex("^[0-9]{10}$") - }, - new() - { - Country = "DE", - Code = "eu_vat", - Description = "European VAT number (Germany)", - Example = "DE123456789", - ValidationExpression = new Regex("^DE[0-9]{9}$") - }, - new() - { - Country = "GR", - Code = "eu_vat", - Description = "European VAT number (Greece)", - Example = "EL123456789", - ValidationExpression = new Regex("^EL[0-9]{9}$") - }, - new() - { - Country = "HK", - Code = "hk_br", - Description = "Hong Kong BR number", - Example = "12345678", - ValidationExpression = new Regex("^[0-9]{8}$") - }, - new() - { - Country = "HU", - Code = "eu_vat", - Description = "European VAT number (Hungaria)", - Example = "HU12345678", - ValidationExpression = new Regex("^HU[0-9]{8}$") - }, - new() - { - Country = "HU", - Code = "hu_tin", - Description = "Hungary tax number (adószám)", - Example = "12345678-1-23", - ValidationExpression = new Regex("^[0-9]{8}-?[0-9]-?[0-9]{2}$") - }, - new() - { - Country = "IS", - Code = "is_vat", - Description = "Icelandic VAT", - Example = "123456", - ValidationExpression = new Regex("^[0-9]{6}$") - }, - new() - { - Country = "IN", - Code = "in_gst", - Description = "Indian GST number", - Example = "12ABCDE3456FGZH", - ValidationExpression = new Regex("^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$") - }, - new() - { - Country = "ID", - Code = "id_npwp", - Description = "Indonesian NPWP number", - Example = "012.345.678.9-012.345", - ValidationExpression = new Regex("^[0-9]{3}.?[0-9]{3}.?[0-9]{3}.?[0-9]{1}-?[0-9]{3}.?[0-9]{3}$") - }, - new() - { - Country = "IE", - Code = "eu_vat", - Description = "European VAT number (Ireland)", - Example = "IE1234567AB", - ValidationExpression = new Regex("^IE[0-9]{7}[A-Z]{1,2}$") - }, - new() - { - Country = "IL", - Code = "il_vat", - Description = "Israel VAT", - Example = "000012345", - ValidationExpression = new Regex("^[0-9]{9}$") - }, - new() - { - Country = "IT", - Code = "eu_vat", - Description = "European VAT number (Italy)", - Example = "IT12345678912", - ValidationExpression = new Regex("^IT[0-9]{11}$") - }, - new() - { - Country = "JP", - Code = "jp_cn", - Description = "Japanese Corporate Number (*Hōjin Bangō*)", - Example = "1234567891234", - ValidationExpression = new Regex("^[0-9]{13}$") - }, - new() - { - Country = "JP", - Code = "jp_rn", - Description = - "Japanese Registered Foreign Businesses' Registration Number (*Tōroku Kokugai Jigyōsha no Tōroku Bangō*)", - Example = "12345", - ValidationExpression = new Regex("^[0-9]{5}$") - }, - new() - { - Country = "JP", - Code = "jp_trn", - Description = "Japanese Tax Registration Number (*Tōroku Bangō*)", - Example = "T1234567891234", - ValidationExpression = new Regex("^T[0-9]{13}$") - }, - new() - { - Country = "KZ", - Code = "kz_bin", - Description = "Kazakhstani Business Identification Number", - Example = "123456789012", - ValidationExpression = new Regex("^[0-9]{12}$") - }, - new() - { - Country = "KE", - Code = "ke_pin", - Description = "Kenya Revenue Authority Personal Identification Number", - Example = "P000111111A", - ValidationExpression = new Regex("^[A-Z]{1}[0-9]{9}[A-Z]{1}$") - }, - new() - { - Country = "LV", - Code = "eu_vat", - Description = "European VAT number", - Example = "LV12345678912", - ValidationExpression = new Regex("^LV[0-9]{11}$") - }, - new() - { - Country = "LI", - Code = "li_uid", - Description = "Liechtensteinian UID number", - Example = "CHE123456789", - ValidationExpression = new Regex("^CHE[0-9]{9}$") - }, - new() - { - Country = "LI", - Code = "li_vat", - Description = "Liechtensteinian VAT number", - Example = "12345", - ValidationExpression = new Regex("^[0-9]{5}$") - }, - new() - { - Country = "LT", - Code = "eu_vat", - Description = "European VAT number (Lithuania)", - Example = "LT123456789123", - ValidationExpression = new Regex("^LT[0-9]{9,12}$") - }, - new() - { - Country = "LU", - Code = "eu_vat", - Description = "European VAT number (Luxembourg)", - Example = "LU12345678", - ValidationExpression = new Regex("^LU[0-9]{8}$") - }, - new() - { - Country = "MY", - Code = "my_frp", - Description = "Malaysian FRP number", - Example = "12345678", - ValidationExpression = new Regex("^[0-9]{8}$") - }, - new() - { - Country = "MY", - Code = "my_itn", - Description = "Malaysian ITN", - Example = "C 1234567890", - ValidationExpression = new Regex("^[A-Z]{1} ?[0-9]{10}$") - }, - new() - { - Country = "MY", - Code = "my_sst", - Description = "Malaysian SST number", - Example = "A12-3456-78912345", - ValidationExpression = new Regex("^[A-Z]{1}[0-9]{2}-?[0-9]{4}-?[0-9]{8}$") - }, - new() - { - Country = "MT", - Code = "eu_vat", - Description = "European VAT number (Malta)", - Example = "MT12345678", - ValidationExpression = new Regex("^MT[0-9]{8}$") - }, - new() - { - Country = "MX", - Code = "mx_rfc", - Description = "Mexican RFC number", - Example = "ABC010203AB9", - ValidationExpression = new Regex("^[A-Z]{3}[0-9]{6}[A-Z0-9]{3}$") - }, - new() - { - Country = "MD", - Code = "md_vat", - Description = "Moldova VAT Number", - Example = "1234567", - ValidationExpression = new Regex("^[0-9]{7}$") - }, - new() - { - Country = "MA", - Code = "ma_vat", - Description = "Morocco VAT Number", - Example = "12345678", - ValidationExpression = new Regex("^[0-9]{8}$") - }, - new() - { - Country = "NL", - Code = "eu_vat", - Description = "European VAT number (Netherlands)", - Example = "NL123456789B12", - ValidationExpression = new Regex("^NL[0-9]{9}B[0-9]{2}$") - }, - new() - { - Country = "NZ", - Code = "nz_gst", - Description = "New Zealand GST number", - Example = "123456789", - ValidationExpression = new Regex("^[0-9]{9}$") - }, - new() - { - Country = "NG", - Code = "ng_tin", - Description = "Nigerian TIN Number", - Example = "12345678-0001", - ValidationExpression = new Regex("^[0-9]{8}-[0-9]{4}$") - }, - new() - { - Country = "NO", - Code = "no_vat", - Description = "Norwegian VAT number", - Example = "123456789MVA", - ValidationExpression = new Regex("^[0-9]{9}MVA$") - }, - new() - { - Country = "NO", - Code = "no_voec", - Description = "Norwegian VAT on e-commerce number", - Example = "1234567", - ValidationExpression = new Regex("^[0-9]{7}$") - }, - new() - { - Country = "OM", - Code = "om_vat", - Description = "Omani VAT Number", - Example = "OM1234567890", - ValidationExpression = new Regex("^OM[0-9]{10}$") - }, - new() - { - Country = "PE", - Code = "pe_ruc", - Description = "Peruvian RUC number", - Example = "12345678901", - ValidationExpression = new Regex("^[0-9]{11}$") - }, - new() - { - Country = "PH", - Code = "ph_tin", - Description = "Philippines Tax Identification Number", - Example = "123456789012", - ValidationExpression = new Regex("^[0-9]{12}$") - }, - new() - { - Country = "PL", - Code = "eu_vat", - Description = "European VAT number (Poland)", - Example = "PL1234567890", - ValidationExpression = new Regex("^PL[0-9]{10}$") - }, - new() - { - Country = "PT", - Code = "eu_vat", - Description = "European VAT number (Portugal)", - Example = "PT123456789", - ValidationExpression = new Regex("^PT[0-9]{9}$") - }, - new() - { - Country = "RO", - Code = "eu_vat", - Description = "European VAT number (Romania)", - Example = "RO1234567891", - ValidationExpression = new Regex("^RO[0-9]{2,10}$") - }, - new() - { - Country = "RO", - Code = "ro_tin", - Description = "Romanian tax ID number", - Example = "1234567890123", - ValidationExpression = new Regex("^[0-9]{13}$") - }, - new() - { - Country = "RU", - Code = "ru_inn", - Description = "Russian INN", - Example = "1234567891", - ValidationExpression = new Regex("^[0-9]{10,12}$") - }, - new() - { - Country = "RU", - Code = "ru_kpp", - Description = "Russian KPP", - Example = "123456789", - ValidationExpression = new Regex("^[0-9]{9}$") - }, - new() - { - Country = "SA", - Code = "sa_vat", - Description = "Saudi Arabia VAT", - Example = "123456789012345", - ValidationExpression = new Regex("^[0-9]{15}$") - }, - new() - { - Country = "RS", - Code = "rs_pib", - Description = "Serbian PIB number", - Example = "123456789", - ValidationExpression = new Regex("^[0-9]{9}$") - }, - new() - { - Country = "SG", - Code = "sg_gst", - Description = "Singaporean GST", - Example = "M12345678X", - ValidationExpression = new Regex("^[A-Z]{1}[0-9]{8}[A-Z]{1}$") - }, - new() - { - Country = "SG", - Code = "sg_uen", - Description = "Singaporean UEN", - Example = "123456789F", - ValidationExpression = new Regex("^[0-9]{9}[A-Z]{1}$") - }, - new() - { - Country = "SK", - Code = "eu_vat", - Description = "European VAT number (Slovakia)", - Example = "SK1234567891", - ValidationExpression = new Regex("^SK[0-9]{10}$") - }, - new() - { - Country = "SI", - Code = "eu_vat", - Description = "European VAT number (Slovenia)", - Example = "SI12345678", - ValidationExpression = new Regex("^SI[0-9]{8}$") - }, - new() - { - Country = "SI", - Code = "si_tin", - Description = "Slovenia tax number (davčna številka)", - Example = "12345678", - ValidationExpression = new Regex("^[0-9]{8}$") - }, - new() - { - Country = "ZA", - Code = "za_vat", - Description = "South African VAT number", - Example = "4123456789", - ValidationExpression = new Regex("^[0-9]{10}$") - }, - new() - { - Country = "KR", - Code = "kr_brn", - Description = "Korean BRN", - Example = "123-45-67890", - ValidationExpression = new Regex("^[0-9]{3}-?[0-9]{2}-?[0-9]{5}$") - }, - new() - { - Country = "ES", - Code = "es_cif", - Description = "Spanish NIF/CIF number", - Example = "A12345678", - ValidationExpression = new Regex("^[A-Z]{1}[0-9]{8}$") - }, - new() - { - Country = "ES", - Code = "eu_vat", - Description = "European VAT number (Spain)", - Example = "ESA1234567Z", - ValidationExpression = new Regex("^ES[A-Z]{1}[0-9]{7}[A-Z]{1}$") - }, - new() - { - Country = "SE", - Code = "eu_vat", - Description = "European VAT number (Sweden)", - Example = "SE123456789123", - ValidationExpression = new Regex("^SE[0-9]{12}$") - }, - new() - { - Country = "CH", - Code = "ch_uid", - Description = "Switzerland UID number", - Example = "CHE-123.456.789 HR", - ValidationExpression = new Regex("^CHE-?[0-9]{3}.?[0-9]{3}.?[0-9]{3} ?HR$") - }, - new() - { - Country = "CH", - Code = "ch_vat", - Description = "Switzerland VAT number", - Example = "CHE-123.456.789 MWST", - ValidationExpression = new Regex("^CHE-?[0-9]{3}.?[0-9]{3}.?[0-9]{3} ?MWST$") - }, - new() - { - Country = "TW", - Code = "tw_vat", - Description = "Taiwanese VAT", - Example = "12345678", - ValidationExpression = new Regex("^[0-9]{8}$") - }, - new() - { - Country = "TZ", - Code = "tz_vat", - Description = "Tanzania VAT Number", - Example = "12345678A", - ValidationExpression = new Regex("^[0-9]{8}[A-Z]{1}$") - }, - new() - { - Country = "TH", - Code = "th_vat", - Description = "Thai VAT", - Example = "1234567891234", - ValidationExpression = new Regex("^[0-9]{13}$") - }, - new() - { - Country = "TR", - Code = "tr_tin", - Description = "Turkish TIN Number", - Example = "0123456789", - ValidationExpression = new Regex("^[0-9]{10}$") - }, - new() - { - Country = "UA", - Code = "ua_vat", - Description = "Ukrainian VAT", - Example = "123456789", - ValidationExpression = new Regex("^[0-9]{9}$") - }, - new() - { - Country = "AE", - Code = "ae_trn", - Description = "United Arab Emirates TRN", - Example = "123456789012345", - ValidationExpression = new Regex("^[0-9]{15}$") - }, - new() - { - Country = "GB", - Code = "eu_vat", - Description = "Northern Ireland VAT number", - Example = "XI123456789", - ValidationExpression = new Regex("^XI[0-9]{9}$") - }, - new() - { - Country = "GB", - Code = "gb_vat", - Description = "United Kingdom VAT number", - Example = "GB123456789", - ValidationExpression = new Regex("^GB[0-9]{9}$") - }, - new() - { - Country = "US", - Code = "us_ein", - Description = "United States EIN", - Example = "12-3456789", - ValidationExpression = new Regex("^[0-9]{2}-?[0-9]{7}$") - }, - new() - { - Country = "UY", - Code = "uy_ruc", - Description = "Uruguayan RUC number", - Example = "123456789012", - ValidationExpression = new Regex("^[0-9]{12}$") - }, - new() - { - Country = "UZ", - Code = "uz_tin", - Description = "Uzbekistan TIN Number", - Example = "123456789", - ValidationExpression = new Regex("^[0-9]{9}$") - }, - new() - { - Country = "UZ", - Code = "uz_vat", - Description = "Uzbekistan VAT Number", - Example = "123456789012", - ValidationExpression = new Regex("^[0-9]{12}$") - }, - new() - { - Country = "VE", - Code = "ve_rif", - Description = "Venezuelan RIF number", - Example = "A-12345678-9", - ValidationExpression = new Regex("^[A-Z]{1}-?[0-9]{8}-?[0-9]{1}$") - }, - new() - { - Country = "VN", - Code = "vn_tin", - Description = "Vietnamese tax ID number", - Example = "1234567890", - ValidationExpression = new Regex("^[0-9]{10}$") - } - ]; - - public string GetStripeTaxCode(string country, string taxId) - { - foreach (var taxIdType in _taxIdTypes.Where(x => x.Country == country)) - { - if (taxIdType.ValidationExpression.IsMatch(taxId)) - { - return taxIdType.Code; - } - } - - return null; - } - - public bool IsSupported(string country) - { - return _taxIdTypes.Any(x => x.Country == country); - } -} diff --git a/src/Core/Billing/Utilities.cs b/src/Core/Billing/Utilities.cs index 695a3b1bb4..28527af0c0 100644 --- a/src/Core/Billing/Utilities.cs +++ b/src/Core/Billing/Utilities.cs @@ -83,7 +83,6 @@ public static class Utilities customer.Address.Country, customer.Address.PostalCode, customer.TaxIds?.FirstOrDefault()?.Value, - customer.TaxIds?.FirstOrDefault()?.Type, customer.Address.Line1, customer.Address.Line2, customer.Address.City, diff --git a/src/Core/Models/Business/TaxInfo.cs b/src/Core/Models/Business/TaxInfo.cs index b12c5229b3..4424576ec9 100644 --- a/src/Core/Models/Business/TaxInfo.cs +++ b/src/Core/Models/Business/TaxInfo.cs @@ -2,9 +2,18 @@ public class TaxInfo { - public string TaxIdNumber { get; set; } - public string TaxIdType { get; set; } + private string _taxIdNumber = null; + private string _taxIdType = null; + public string TaxIdNumber + { + get => _taxIdNumber; + set + { + _taxIdNumber = value; + _taxIdType = null; + } + } public string StripeTaxRateId { get; set; } public string BillingAddressLine1 { get; set; } public string BillingAddressLine2 { get; set; } @@ -12,6 +21,201 @@ public class TaxInfo public string BillingAddressState { get; set; } public string BillingAddressPostalCode { get; set; } public string BillingAddressCountry { get; set; } = "US"; + public string TaxIdType + { + get + { + if (string.IsNullOrWhiteSpace(BillingAddressCountry) || + string.IsNullOrWhiteSpace(TaxIdNumber)) + { + return null; + } + if (!string.IsNullOrWhiteSpace(_taxIdType)) + { + return _taxIdType; + } + + switch (BillingAddressCountry.ToUpper()) + { + case "AD": + _taxIdType = "ad_nrt"; + break; + case "AE": + _taxIdType = "ae_trn"; + break; + case "AR": + _taxIdType = "ar_cuit"; + break; + case "AU": + _taxIdType = "au_abn"; + break; + case "BO": + _taxIdType = "bo_tin"; + break; + case "BR": + _taxIdType = "br_cnpj"; + break; + case "CA": + // May break for those in Québec given the assumption of QST + if (BillingAddressState?.Contains("bec") ?? false) + { + _taxIdType = "ca_qst"; + break; + } + _taxIdType = "ca_bn"; + break; + case "CH": + _taxIdType = "ch_vat"; + break; + case "CL": + _taxIdType = "cl_tin"; + break; + case "CN": + _taxIdType = "cn_tin"; + break; + case "CO": + _taxIdType = "co_nit"; + break; + case "CR": + _taxIdType = "cr_tin"; + break; + case "DO": + _taxIdType = "do_rcn"; + break; + case "EC": + _taxIdType = "ec_ruc"; + break; + case "EG": + _taxIdType = "eg_tin"; + break; + case "GE": + _taxIdType = "ge_vat"; + break; + case "ID": + _taxIdType = "id_npwp"; + break; + case "IL": + _taxIdType = "il_vat"; + break; + case "IS": + _taxIdType = "is_vat"; + break; + case "KE": + _taxIdType = "ke_pin"; + break; + case "AT": + case "BE": + case "BG": + case "CY": + case "CZ": + case "DE": + case "DK": + case "EE": + case "ES": + case "FI": + case "FR": + case "GB": + case "GR": + case "HR": + case "HU": + case "IE": + case "IT": + case "LT": + case "LU": + case "LV": + case "MT": + case "NL": + case "PL": + case "PT": + case "RO": + case "SE": + case "SI": + case "SK": + _taxIdType = "eu_vat"; + break; + case "HK": + _taxIdType = "hk_br"; + break; + case "IN": + _taxIdType = "in_gst"; + break; + case "JP": + _taxIdType = "jp_cn"; + break; + case "KR": + _taxIdType = "kr_brn"; + break; + case "LI": + _taxIdType = "li_uid"; + break; + case "MX": + _taxIdType = "mx_rfc"; + break; + case "MY": + _taxIdType = "my_sst"; + break; + case "NO": + _taxIdType = "no_vat"; + break; + case "NZ": + _taxIdType = "nz_gst"; + break; + case "PE": + _taxIdType = "pe_ruc"; + break; + case "PH": + _taxIdType = "ph_tin"; + break; + case "RS": + _taxIdType = "rs_pib"; + break; + case "RU": + _taxIdType = "ru_inn"; + break; + case "SA": + _taxIdType = "sa_vat"; + break; + case "SG": + _taxIdType = "sg_gst"; + break; + case "SV": + _taxIdType = "sv_nit"; + break; + case "TH": + _taxIdType = "th_vat"; + break; + case "TR": + _taxIdType = "tr_tin"; + break; + case "TW": + _taxIdType = "tw_vat"; + break; + case "UA": + _taxIdType = "ua_vat"; + break; + case "US": + _taxIdType = "us_ein"; + break; + case "UY": + _taxIdType = "uy_ruc"; + break; + case "VE": + _taxIdType = "ve_rif"; + break; + case "VN": + _taxIdType = "vn_tin"; + break; + case "ZA": + _taxIdType = "za_vat"; + break; + default: + _taxIdType = null; + break; + } + + return _taxIdType; + } + } public bool HasTaxId { diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index 7d0f9d3c63..bf9d047029 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -1,9 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing.Models; -using Bit.Core.Billing.Models.Api.Requests.Accounts; -using Bit.Core.Billing.Models.Api.Requests.Organizations; -using Bit.Core.Billing.Models.Api.Responses; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; @@ -62,7 +59,4 @@ public interface IPaymentService Task RisksSubscriptionFailure(Organization organization); Task HasSecretsManagerStandalone(Organization organization); Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Stripe.Subscription subscription); - Task PreviewInvoiceAsync(PreviewIndividualInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId); - Task PreviewInvoiceAsync(PreviewOrganizationInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId); - } diff --git a/src/Core/Services/IStripeAdapter.cs b/src/Core/Services/IStripeAdapter.cs index ef2e3ab766..30583ef0b3 100644 --- a/src/Core/Services/IStripeAdapter.cs +++ b/src/Core/Services/IStripeAdapter.cs @@ -31,7 +31,6 @@ public interface IStripeAdapter Task InvoiceUpcomingAsync(Stripe.UpcomingInvoiceOptions options); Task InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options); Task> InvoiceListAsync(StripeInvoiceListOptions options); - Task InvoiceCreatePreviewAsync(InvoiceCreatePreviewOptions options); Task> InvoiceSearchAsync(InvoiceSearchOptions options); Task InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options); Task InvoiceFinalizeInvoiceAsync(string id, Stripe.InvoiceFinalizeOptions options); @@ -43,7 +42,6 @@ public interface IStripeAdapter IAsyncEnumerable PaymentMethodListAutoPagingAsync(Stripe.PaymentMethodListOptions options); Task PaymentMethodAttachAsync(string id, Stripe.PaymentMethodAttachOptions options = null); Task PaymentMethodDetachAsync(string id, Stripe.PaymentMethodDetachOptions options = null); - Task PlanGetAsync(string id, Stripe.PlanGetOptions options = null); Task TaxRateCreateAsync(Stripe.TaxRateCreateOptions options); Task TaxRateUpdateAsync(string id, Stripe.TaxRateUpdateOptions options); Task TaxIdCreateAsync(string id, Stripe.TaxIdCreateOptions options); diff --git a/src/Core/Services/Implementations/StripeAdapter.cs b/src/Core/Services/Implementations/StripeAdapter.cs index f4f8efe75f..8d18331456 100644 --- a/src/Core/Services/Implementations/StripeAdapter.cs +++ b/src/Core/Services/Implementations/StripeAdapter.cs @@ -15,7 +15,6 @@ public class StripeAdapter : IStripeAdapter private readonly Stripe.RefundService _refundService; private readonly Stripe.CardService _cardService; private readonly Stripe.BankAccountService _bankAccountService; - private readonly Stripe.PlanService _planService; private readonly Stripe.PriceService _priceService; private readonly Stripe.SetupIntentService _setupIntentService; private readonly Stripe.TestHelpers.TestClockService _testClockService; @@ -34,7 +33,6 @@ public class StripeAdapter : IStripeAdapter _cardService = new Stripe.CardService(); _bankAccountService = new Stripe.BankAccountService(); _priceService = new Stripe.PriceService(); - _planService = new Stripe.PlanService(); _setupIntentService = new SetupIntentService(); _testClockService = new Stripe.TestHelpers.TestClockService(); _customerBalanceTransactionService = new CustomerBalanceTransactionService(); @@ -135,11 +133,6 @@ public class StripeAdapter : IStripeAdapter return invoices; } - public Task InvoiceCreatePreviewAsync(InvoiceCreatePreviewOptions options) - { - return _invoiceService.CreatePreviewAsync(options); - } - public async Task> InvoiceSearchAsync(InvoiceSearchOptions options) => (await _invoiceService.SearchAsync(options)).Data; @@ -191,11 +184,6 @@ public class StripeAdapter : IStripeAdapter return _paymentMethodService.DetachAsync(id, options); } - public Task PlanGetAsync(string id, Stripe.PlanGetOptions options = null) - { - return _planService.GetAsync(id, options); - } - public Task TaxRateCreateAsync(Stripe.TaxRateCreateOptions options) { return _taxRateService.CreateAsync(options); diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index c32dcd43ad..259a4eb757 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -1,13 +1,8 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; -using Bit.Core.Billing.Models.Api.Requests.Accounts; -using Bit.Core.Billing.Models.Api.Requests.Organizations; -using Bit.Core.Billing.Models.Api.Responses; using Bit.Core.Billing.Models.Business; -using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -37,7 +32,6 @@ public class StripePaymentService : IPaymentService private readonly IStripeAdapter _stripeAdapter; private readonly IGlobalSettings _globalSettings; private readonly IFeatureService _featureService; - private readonly ITaxService _taxService; public StripePaymentService( ITransactionRepository transactionRepository, @@ -46,8 +40,7 @@ public class StripePaymentService : IPaymentService IStripeAdapter stripeAdapter, Braintree.IBraintreeGateway braintreeGateway, IGlobalSettings globalSettings, - IFeatureService featureService, - ITaxService taxService) + IFeatureService featureService) { _transactionRepository = transactionRepository; _logger = logger; @@ -56,7 +49,6 @@ public class StripePaymentService : IPaymentService _btGateway = braintreeGateway; _globalSettings = globalSettings; _featureService = featureService; - _taxService = taxService; } public async Task PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, @@ -120,20 +112,6 @@ public class StripePaymentService : IPaymentService Subscription subscription; try { - if (taxInfo.TaxIdNumber != null && taxInfo.TaxIdType == null) - { - taxInfo.TaxIdType = _taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry, - taxInfo.TaxIdNumber); - - if (taxInfo.TaxIdType == null) - { - _logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", - taxInfo.BillingAddressCountry, - taxInfo.TaxIdNumber); - throw new BadRequestException("billingTaxIdTypeInferenceError"); - } - } - var customerCreateOptions = new CustomerCreateOptions { Description = org.DisplayBusinessName(), @@ -168,9 +146,12 @@ public class StripePaymentService : IPaymentService City = taxInfo?.BillingAddressCity, State = taxInfo?.BillingAddressState, }, - TaxIdData = taxInfo.HasTaxId - ? [new CustomerTaxIdDataOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber }] - : null + TaxIdData = taxInfo?.HasTaxId != true + ? null + : + [ + new CustomerTaxIdDataOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber, } + ], }; customerCreateOptions.AddExpand("tax"); @@ -1678,7 +1659,6 @@ public class StripePaymentService : IPaymentService return new TaxInfo { TaxIdNumber = taxId?.Value, - TaxIdType = taxId?.Type, BillingAddressLine1 = address?.Line1, BillingAddressLine2 = address?.Line2, BillingAddressCity = address?.City, @@ -1690,13 +1670,9 @@ public class StripePaymentService : IPaymentService public async Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo) { - if (string.IsNullOrWhiteSpace(subscriber?.GatewayCustomerId) || subscriber.IsUser()) + if (subscriber != null && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) { - return; - } - - var customer = await _stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, - new CustomerUpdateOptions + var customer = await _stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, new CustomerUpdateOptions { Address = new AddressOptions { @@ -1710,59 +1686,23 @@ public class StripePaymentService : IPaymentService Expand = ["tax_ids"] }); - if (customer == null) - { - return; - } - - var taxId = customer.TaxIds?.FirstOrDefault(); - - if (taxId != null) - { - await _stripeAdapter.TaxIdDeleteAsync(customer.Id, taxId.Id); - } - - if (string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber)) - { - return; - } - - var taxIdType = taxInfo.TaxIdType; - - if (string.IsNullOrWhiteSpace(taxIdType)) - { - taxIdType = _taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry, taxInfo.TaxIdNumber); - - if (taxIdType == null) + if (!subscriber.IsUser() && customer != null) { - _logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", - taxInfo.BillingAddressCountry, - taxInfo.TaxIdNumber); - throw new BadRequestException("billingTaxIdTypeInferenceError"); - } - } + var taxId = customer.TaxIds?.FirstOrDefault(); - try - { - await _stripeAdapter.TaxIdCreateAsync(customer.Id, - new TaxIdCreateOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber, }); - } - catch (StripeException e) - { - switch (e.StripeError.Code) - { - case StripeConstants.ErrorCodes.TaxIdInvalid: - _logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", - taxInfo.TaxIdNumber, - taxInfo.BillingAddressCountry); - throw new BadRequestException("billingInvalidTaxIdError"); - default: - _logger.LogError(e, - "Error creating tax ID '{TaxId}' in country '{Country}' for customer '{CustomerID}'.", - taxInfo.TaxIdNumber, - taxInfo.BillingAddressCountry, - customer.Id); - throw new BadRequestException("billingTaxIdCreationError"); + if (taxId != null) + { + await _stripeAdapter.TaxIdDeleteAsync(customer.Id, taxId.Id); + } + if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber) && + !string.IsNullOrWhiteSpace(taxInfo.TaxIdType)) + { + await _stripeAdapter.TaxIdCreateAsync(customer.Id, new TaxIdCreateOptions + { + Type = taxInfo.TaxIdType, + Value = taxInfo.TaxIdNumber, + }); + } } } } @@ -1895,285 +1835,6 @@ public class StripePaymentService : IPaymentService } } - public async Task PreviewInvoiceAsync( - PreviewIndividualInvoiceRequestBody parameters, - string gatewayCustomerId, - string gatewaySubscriptionId) - { - var options = new InvoiceCreatePreviewOptions - { - AutomaticTax = new InvoiceAutomaticTaxOptions - { - Enabled = true, - }, - Currency = "usd", - Discounts = new List(), - SubscriptionDetails = new InvoiceSubscriptionDetailsOptions - { - Items = - [ - new() - { - Quantity = 1, - Plan = "premium-annually" - }, - - new() - { - Quantity = parameters.PasswordManager.AdditionalStorage, - Plan = "storage-gb-annually" - } - ] - }, - CustomerDetails = new InvoiceCustomerDetailsOptions - { - Address = new AddressOptions - { - PostalCode = parameters.TaxInformation.PostalCode, - Country = parameters.TaxInformation.Country, - } - }, - }; - - if (!string.IsNullOrEmpty(parameters.TaxInformation.TaxId)) - { - var taxIdType = _taxService.GetStripeTaxCode( - options.CustomerDetails.Address.Country, - parameters.TaxInformation.TaxId); - - if (taxIdType == null) - { - _logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", - parameters.TaxInformation.TaxId, - parameters.TaxInformation.Country); - throw new BadRequestException("billingPreviewInvalidTaxIdError"); - } - - options.CustomerDetails.TaxIds = [ - new InvoiceCustomerDetailsTaxIdOptions - { - Type = taxIdType, - Value = parameters.TaxInformation.TaxId - } - ]; - } - - if (gatewayCustomerId != null) - { - var gatewayCustomer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId); - - if (gatewayCustomer.Discount != null) - { - options.Discounts.Add(new InvoiceDiscountOptions - { - Discount = gatewayCustomer.Discount.Id - }); - } - - if (gatewaySubscriptionId != null) - { - var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId); - - if (gatewaySubscription?.Discount != null) - { - options.Discounts.Add(new InvoiceDiscountOptions - { - Discount = gatewaySubscription.Discount.Id - }); - } - } - } - - try - { - var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options); - - var effectiveTaxRate = invoice.Tax != null && invoice.TotalExcludingTax != null - ? invoice.Tax.Value.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor() - : 0M; - - var result = new PreviewInvoiceResponseModel( - effectiveTaxRate, - invoice.TotalExcludingTax.ToMajor() ?? 0, - invoice.Tax.ToMajor() ?? 0, - invoice.Total.ToMajor()); - return result; - } - catch (StripeException e) - { - switch (e.StripeError.Code) - { - case StripeConstants.ErrorCodes.TaxIdInvalid: - _logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", - parameters.TaxInformation.TaxId, - parameters.TaxInformation.Country); - throw new BadRequestException("billingPreviewInvalidTaxIdError"); - default: - _logger.LogError(e, "Unexpected error previewing invoice with tax ID '{TaxId}' in country '{Country}'.", - parameters.TaxInformation.TaxId, - parameters.TaxInformation.Country); - throw new BadRequestException("billingPreviewInvoiceError"); - } - } - } - - public async Task PreviewInvoiceAsync( - PreviewOrganizationInvoiceRequestBody parameters, - string gatewayCustomerId, - string gatewaySubscriptionId) - { - var plan = Utilities.StaticStore.GetPlan(parameters.PasswordManager.Plan); - - var options = new InvoiceCreatePreviewOptions - { - AutomaticTax = new InvoiceAutomaticTaxOptions - { - Enabled = true, - }, - Currency = "usd", - Discounts = new List(), - SubscriptionDetails = new InvoiceSubscriptionDetailsOptions - { - Items = - [ - new() - { - Quantity = parameters.PasswordManager.AdditionalStorage, - Plan = plan.PasswordManager.StripeStoragePlanId - } - ] - }, - CustomerDetails = new InvoiceCustomerDetailsOptions - { - Address = new AddressOptions - { - PostalCode = parameters.TaxInformation.PostalCode, - Country = parameters.TaxInformation.Country, - } - }, - }; - - if (plan.PasswordManager.HasAdditionalSeatsOption) - { - options.SubscriptionDetails.Items.Add( - new() - { - Quantity = parameters.PasswordManager.Seats, - Plan = plan.PasswordManager.StripeSeatPlanId - } - ); - } - else - { - options.SubscriptionDetails.Items.Add( - new() - { - Quantity = 1, - Plan = plan.PasswordManager.StripePlanId - } - ); - } - - if (plan.SupportsSecretsManager) - { - if (plan.SecretsManager.HasAdditionalSeatsOption) - { - options.SubscriptionDetails.Items.Add(new() - { - Quantity = parameters.SecretsManager?.Seats ?? 0, - Plan = plan.SecretsManager.StripeSeatPlanId - }); - } - - if (plan.SecretsManager.HasAdditionalServiceAccountOption) - { - options.SubscriptionDetails.Items.Add(new() - { - Quantity = parameters.SecretsManager?.AdditionalMachineAccounts ?? 0, - Plan = plan.SecretsManager.StripeServiceAccountPlanId - }); - } - } - - if (!string.IsNullOrEmpty(parameters.TaxInformation.TaxId)) - { - var taxIdType = _taxService.GetStripeTaxCode( - options.CustomerDetails.Address.Country, - parameters.TaxInformation.TaxId); - - if (taxIdType == null) - { - _logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", - parameters.TaxInformation.TaxId, - parameters.TaxInformation.Country); - throw new BadRequestException("billingTaxIdTypeInferenceError"); - } - - options.CustomerDetails.TaxIds = [ - new InvoiceCustomerDetailsTaxIdOptions - { - Type = taxIdType, - Value = parameters.TaxInformation.TaxId - } - ]; - } - - if (gatewayCustomerId != null) - { - var gatewayCustomer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId); - - if (gatewayCustomer.Discount != null) - { - options.Discounts.Add(new InvoiceDiscountOptions - { - Discount = gatewayCustomer.Discount.Id - }); - } - - var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId); - - if (gatewaySubscription?.Discount != null) - { - options.Discounts.Add(new InvoiceDiscountOptions - { - Discount = gatewaySubscription.Discount.Id - }); - } - } - - try - { - var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options); - - var effectiveTaxRate = invoice.Tax != null && invoice.TotalExcludingTax != null - ? invoice.Tax.Value.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor() - : 0M; - - var result = new PreviewInvoiceResponseModel( - effectiveTaxRate, - invoice.TotalExcludingTax.ToMajor() ?? 0, - invoice.Tax.ToMajor() ?? 0, - invoice.Total.ToMajor()); - return result; - } - catch (StripeException e) - { - switch (e.StripeError.Code) - { - case StripeConstants.ErrorCodes.TaxIdInvalid: - _logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", - parameters.TaxInformation.TaxId, - parameters.TaxInformation.Country); - throw new BadRequestException("billingPreviewInvalidTaxIdError"); - default: - _logger.LogError(e, "Unexpected error previewing invoice with tax ID '{TaxId}' in country '{Country}'.", - parameters.TaxInformation.TaxId, - parameters.TaxInformation.Country); - throw new BadRequestException("billingPreviewInvoiceError"); - } - } - } - private PaymentMethod GetLatestCardPaymentMethod(string customerId) { var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging( diff --git a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs index 9c25ffdc55..385b185ffe 100644 --- a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs +++ b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs @@ -1545,7 +1545,7 @@ public class SubscriberServiceTests { var stripeAdapter = sutProvider.GetDependency(); - var customer = new Customer { Id = provider.GatewayCustomerId, TaxIds = new StripeList { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] } }; + var customer = new Customer { Id = provider.GatewayCustomerId, TaxIds = new StripeList { Data = [new TaxId { Id = "tax_id_1" }] } }; stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId, Arg.Is( options => options.Expand.Contains("tax_ids"))).Returns(customer); @@ -1554,7 +1554,6 @@ public class SubscriberServiceTests "US", "12345", "123456789", - "us_ein", "123 Example St.", null, "Example Town", diff --git a/test/Core.Test/Models/Business/TaxInfoTests.cs b/test/Core.Test/Models/Business/TaxInfoTests.cs new file mode 100644 index 0000000000..197948006e --- /dev/null +++ b/test/Core.Test/Models/Business/TaxInfoTests.cs @@ -0,0 +1,114 @@ +using Bit.Core.Models.Business; +using Xunit; + +namespace Bit.Core.Test.Models.Business; + +public class TaxInfoTests +{ + // PH = Placeholder + [Theory] + [InlineData(null, null, null, null)] + [InlineData("", "", null, null)] + [InlineData("PH", "", null, null)] + [InlineData("", "PH", null, null)] + [InlineData("AE", "PH", null, "ae_trn")] + [InlineData("AU", "PH", null, "au_abn")] + [InlineData("BR", "PH", null, "br_cnpj")] + [InlineData("CA", "PH", "bec", "ca_qst")] + [InlineData("CA", "PH", null, "ca_bn")] + [InlineData("CL", "PH", null, "cl_tin")] + [InlineData("AT", "PH", null, "eu_vat")] + [InlineData("BE", "PH", null, "eu_vat")] + [InlineData("BG", "PH", null, "eu_vat")] + [InlineData("CY", "PH", null, "eu_vat")] + [InlineData("CZ", "PH", null, "eu_vat")] + [InlineData("DE", "PH", null, "eu_vat")] + [InlineData("DK", "PH", null, "eu_vat")] + [InlineData("EE", "PH", null, "eu_vat")] + [InlineData("ES", "PH", null, "eu_vat")] + [InlineData("FI", "PH", null, "eu_vat")] + [InlineData("FR", "PH", null, "eu_vat")] + [InlineData("GB", "PH", null, "eu_vat")] + [InlineData("GR", "PH", null, "eu_vat")] + [InlineData("HR", "PH", null, "eu_vat")] + [InlineData("HU", "PH", null, "eu_vat")] + [InlineData("IE", "PH", null, "eu_vat")] + [InlineData("IT", "PH", null, "eu_vat")] + [InlineData("LT", "PH", null, "eu_vat")] + [InlineData("LU", "PH", null, "eu_vat")] + [InlineData("LV", "PH", null, "eu_vat")] + [InlineData("MT", "PH", null, "eu_vat")] + [InlineData("NL", "PH", null, "eu_vat")] + [InlineData("PL", "PH", null, "eu_vat")] + [InlineData("PT", "PH", null, "eu_vat")] + [InlineData("RO", "PH", null, "eu_vat")] + [InlineData("SE", "PH", null, "eu_vat")] + [InlineData("SI", "PH", null, "eu_vat")] + [InlineData("SK", "PH", null, "eu_vat")] + [InlineData("HK", "PH", null, "hk_br")] + [InlineData("IN", "PH", null, "in_gst")] + [InlineData("JP", "PH", null, "jp_cn")] + [InlineData("KR", "PH", null, "kr_brn")] + [InlineData("LI", "PH", null, "li_uid")] + [InlineData("MX", "PH", null, "mx_rfc")] + [InlineData("MY", "PH", null, "my_sst")] + [InlineData("NO", "PH", null, "no_vat")] + [InlineData("NZ", "PH", null, "nz_gst")] + [InlineData("RU", "PH", null, "ru_inn")] + [InlineData("SA", "PH", null, "sa_vat")] + [InlineData("SG", "PH", null, "sg_gst")] + [InlineData("TH", "PH", null, "th_vat")] + [InlineData("TW", "PH", null, "tw_vat")] + [InlineData("US", "PH", null, "us_ein")] + [InlineData("ZA", "PH", null, "za_vat")] + [InlineData("ABCDEF", "PH", null, null)] + public void GetTaxIdType_Success(string billingAddressCountry, + string taxIdNumber, + string billingAddressState, + string expectedTaxIdType) + { + var taxInfo = new TaxInfo + { + BillingAddressCountry = billingAddressCountry, + TaxIdNumber = taxIdNumber, + BillingAddressState = billingAddressState, + }; + + Assert.Equal(expectedTaxIdType, taxInfo.TaxIdType); + } + + [Fact] + public void GetTaxIdType_CreateOnce_ReturnCacheSecondTime() + { + var taxInfo = new TaxInfo + { + BillingAddressCountry = "US", + TaxIdNumber = "PH", + BillingAddressState = null, + }; + + Assert.Equal("us_ein", taxInfo.TaxIdType); + + // Per the current spec even if the values change to something other than null it + // will return the cached version of TaxIdType. + taxInfo.BillingAddressCountry = "ZA"; + + Assert.Equal("us_ein", taxInfo.TaxIdType); + } + + [Theory] + [InlineData(null, null, false)] + [InlineData("123", "US", true)] + [InlineData("123", "ZQ12", false)] + [InlineData(" ", "US", false)] + public void HasTaxId_ReturnsExpected(string taxIdNumber, string billingAddressCountry, bool expected) + { + var taxInfo = new TaxInfo + { + TaxIdNumber = taxIdNumber, + BillingAddressCountry = billingAddressCountry, + }; + + Assert.Equal(expected, taxInfo.HasTaxId); + } +} diff --git a/test/Core.Test/Services/StripePaymentServiceTests.cs b/test/Core.Test/Services/StripePaymentServiceTests.cs index 35e1901a2f..e15f07b113 100644 --- a/test/Core.Test/Services/StripePaymentServiceTests.cs +++ b/test/Core.Test/Services/StripePaymentServiceTests.cs @@ -77,8 +77,7 @@ public class StripePaymentServiceTests c.Address.Line2 == taxInfo.BillingAddressLine2 && c.Address.City == taxInfo.BillingAddressCity && c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData.First().Value == taxInfo.TaxIdNumber && - c.TaxIdData.First().Type == taxInfo.TaxIdType + c.TaxIdData == null )); await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => @@ -135,8 +134,7 @@ public class StripePaymentServiceTests c.Address.Line2 == taxInfo.BillingAddressLine2 && c.Address.City == taxInfo.BillingAddressCity && c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData.First().Value == taxInfo.TaxIdNumber && - c.TaxIdData.First().Type == taxInfo.TaxIdType + c.TaxIdData == null )); await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => @@ -192,8 +190,7 @@ public class StripePaymentServiceTests c.Address.Line2 == taxInfo.BillingAddressLine2 && c.Address.City == taxInfo.BillingAddressCity && c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData.First().Value == taxInfo.TaxIdNumber && - c.TaxIdData.First().Type == taxInfo.TaxIdType + c.TaxIdData == null )); await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => @@ -250,8 +247,7 @@ public class StripePaymentServiceTests c.Address.Line2 == taxInfo.BillingAddressLine2 && c.Address.City == taxInfo.BillingAddressCity && c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData.First().Value == taxInfo.TaxIdNumber && - c.TaxIdData.First().Type == taxInfo.TaxIdType + c.TaxIdData == null )); await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => @@ -445,8 +441,7 @@ public class StripePaymentServiceTests c.Address.Line2 == taxInfo.BillingAddressLine2 && c.Address.City == taxInfo.BillingAddressCity && c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData.First().Value == taxInfo.TaxIdNumber && - c.TaxIdData.First().Type == taxInfo.TaxIdType + c.TaxIdData == null )); await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => @@ -515,8 +510,7 @@ public class StripePaymentServiceTests c.Address.Line2 == taxInfo.BillingAddressLine2 && c.Address.City == taxInfo.BillingAddressCity && c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData.First().Value == taxInfo.TaxIdNumber && - c.TaxIdData.First().Type == taxInfo.TaxIdType + c.TaxIdData == null )); await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => From 3c75ff335b3faee4f924aa97ea089a8454cc4698 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:36:37 -0500 Subject: [PATCH 32/94] [PM-15536] Allow reseller to add organization (#5111) * Allow reseller to add organization * Run dotnet format --- .../AdminConsole/Services/ProviderService.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs index e384d71df9..864466ad45 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs @@ -27,7 +27,11 @@ namespace Bit.Commercial.Core.AdminConsole.Services; public class ProviderService : IProviderService { - public static PlanType[] ProviderDisallowedOrganizationTypes = new[] { PlanType.Free, PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2019 }; + private static readonly PlanType[] _resellerDisallowedOrganizationTypes = [ + PlanType.Free, + PlanType.FamiliesAnnually, + PlanType.FamiliesAnnually2019 + ]; private readonly IDataProtector _dataProtector; private readonly IMailService _mailService; @@ -690,13 +694,14 @@ public class ProviderService : IProviderService throw new BadRequestException($"Multi-organization Enterprise Providers cannot manage organizations with the plan type {requestedType}. Only Enterprise (Monthly) and Enterprise (Annually) are allowed."); } break; + case ProviderType.Reseller: + if (_resellerDisallowedOrganizationTypes.Contains(requestedType)) + { + throw new BadRequestException($"Providers cannot manage organizations with the requested plan type ({requestedType}). Only Teams and Enterprise accounts are allowed."); + } + break; default: throw new BadRequestException($"Unsupported provider type {providerType}."); } - - if (ProviderDisallowedOrganizationTypes.Contains(requestedType)) - { - throw new BadRequestException($"Providers cannot manage organizations with the requested plan type ({requestedType}). Only Teams and Enterprise accounts are allowed."); - } } } From 74e86935a45dee256154a6fad92f80f54f3a4887 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:19:10 -0600 Subject: [PATCH 33/94] add `PM9111ExtensionPersistAddEditForm` feature flag (#5106) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 6efdedfeed..1d807149a9 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -155,6 +155,7 @@ public static class FeatureFlagKeys public const string PM14401_ScaleMSPOnClientOrganizationUpdate = "PM-14401-scale-msp-on-client-organization-update"; public const string DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship"; public const string MacOsNativeCredentialSync = "macos-native-credential-sync"; + public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; public static List GetAllKeys() { From 0e32dcccadd759e938542dc8069bc7be9b4e297b Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Wed, 4 Dec 2024 14:42:12 -0500 Subject: [PATCH 34/94] Update Constants.cs (#5112) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 1d807149a9..46ed985cb8 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -156,6 +156,7 @@ public static class FeatureFlagKeys public const string DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship"; public const string MacOsNativeCredentialSync = "macos-native-credential-sync"; public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; + public const string InlineMenuTotp = "inline-menu-totp"; public static List GetAllKeys() { From 04cf513d7868085e0c954b5bd2220991e75cd19a Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Thu, 5 Dec 2024 09:31:14 -0500 Subject: [PATCH 35/94] [PM-11516] Initial license file refactor (#5002) * Added the ability to create a JWT on an organization license that contains all license properties as claims * Added the ability to create a JWT on a user license that contains all license properties as claims * Added ability to consume JWT licenses * Resolved generic type issues when getting claim value * Now validating the jwt signature, exp, and iat * Moved creation of ClaimsPrincipal outside of licenses given dependecy on cert * Ran dotnet format. Resolved identity error * Updated claim types to use string constants * Updated jwt expires to be one year * Fixed bug requiring email verification to be on the token * dotnet format * Patch build process --------- Co-authored-by: Matt Bishop --- .../Implementations/OrganizationService.cs | 3 +- .../Extensions/ServiceCollectionExtensions.cs | 2 + .../Licenses/Extensions/LicenseExtensions.cs | 151 ++++++++++ .../LicenseServiceCollectionExtensions.cs | 16 + src/Core/Billing/Licenses/LicenseConstants.cs | 58 ++++ .../Billing/Licenses/Models/LicenseContext.cs | 10 + .../Services/ILicenseClaimsFactory.cs | 9 + .../OrganizationLicenseClaimsFactory.cs | 75 +++++ .../UserLicenseClaimsFactory.cs | 37 +++ src/Core/Constants.cs | 1 + src/Core/Models/Business/ILicense.cs | 1 + .../Models/Business/OrganizationLicense.cs | 273 +++++++++++++----- src/Core/Models/Business/UserLicense.cs | 83 +++++- .../Cloud/CloudGetOrganizationLicenseQuery.cs | 6 +- .../UpdateOrganizationLicenseCommand.cs | 3 +- src/Core/Services/ILicensingService.cs | 10 +- .../Implementations/LicensingService.cs | 145 +++++++++- .../Services/Implementations/UserService.cs | 21 +- .../NoopLicensingService.cs | 18 +- .../Utilities/ServiceCollectionExtensions.cs | 2 +- .../Business/OrganizationLicenseTests.cs | 7 +- .../UpdateOrganizationLicenseCommandTests.cs | 15 +- test/Core.Test/Services/UserServiceTests.cs | 6 +- 23 files changed, 846 insertions(+), 106 deletions(-) create mode 100644 src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs create mode 100644 src/Core/Billing/Licenses/Extensions/LicenseServiceCollectionExtensions.cs create mode 100644 src/Core/Billing/Licenses/LicenseConstants.cs create mode 100644 src/Core/Billing/Licenses/Models/LicenseContext.cs create mode 100644 src/Core/Billing/Licenses/Services/ILicenseClaimsFactory.cs create mode 100644 src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs create mode 100644 src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index e8756fb325..862b566c91 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -642,7 +642,8 @@ public class OrganizationService : IOrganizationService OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey, string privateKey) { - var canUse = license.CanUse(_globalSettings, _licensingService, out var exception); + var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license); + var canUse = license.CanUse(_globalSettings, _licensingService, claimsPrincipal, out var exception); if (!canUse) { throw new BadRequestException(exception); diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index abfceac736..78253f7399 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Bit.Core.Billing.Caches; using Bit.Core.Billing.Caches.Implementations; +using Bit.Core.Billing.Licenses.Extensions; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Implementations; @@ -15,5 +16,6 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddLicenseServices(); } } diff --git a/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs new file mode 100644 index 0000000000..184d8dad23 --- /dev/null +++ b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs @@ -0,0 +1,151 @@ +using System.Security.Claims; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Enums; +using Bit.Core.Models.Business; + +namespace Bit.Core.Billing.Licenses.Extensions; + +public static class LicenseExtensions +{ + public static DateTime CalculateFreshExpirationDate(this Organization org, SubscriptionInfo subscriptionInfo) + { + if (subscriptionInfo?.Subscription == null) + { + if (org.PlanType == PlanType.Custom && org.ExpirationDate.HasValue) + { + return org.ExpirationDate.Value; + } + + return DateTime.UtcNow.AddDays(7); + } + + var subscription = subscriptionInfo.Subscription; + + if (subscription.TrialEndDate > DateTime.UtcNow) + { + return subscription.TrialEndDate.Value; + } + + if (org.ExpirationDate.HasValue && org.ExpirationDate.Value < DateTime.UtcNow) + { + return org.ExpirationDate.Value; + } + + if (subscription.PeriodEndDate.HasValue && subscription.PeriodDuration > TimeSpan.FromDays(180)) + { + return subscription.PeriodEndDate + .Value + .AddDays(Bit.Core.Constants.OrganizationSelfHostSubscriptionGracePeriodDays); + } + + return org.ExpirationDate?.AddMonths(11) ?? DateTime.UtcNow.AddYears(1); + } + + public static DateTime CalculateFreshRefreshDate(this Organization org, SubscriptionInfo subscriptionInfo, DateTime expirationDate) + { + if (subscriptionInfo?.Subscription == null || + subscriptionInfo.Subscription.TrialEndDate > DateTime.UtcNow || + org.ExpirationDate < DateTime.UtcNow) + { + return expirationDate; + } + + return subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180) || + DateTime.UtcNow - expirationDate > TimeSpan.FromDays(30) + ? DateTime.UtcNow.AddDays(30) + : expirationDate; + } + + public static DateTime CalculateFreshExpirationDateWithoutGracePeriod(this Organization org, SubscriptionInfo subscriptionInfo, DateTime expirationDate) + { + if (subscriptionInfo?.Subscription is null) + { + return expirationDate; + } + + var subscription = subscriptionInfo.Subscription; + + if (subscription.TrialEndDate <= DateTime.UtcNow && + org.ExpirationDate >= DateTime.UtcNow && + subscription.PeriodEndDate.HasValue && + subscription.PeriodDuration > TimeSpan.FromDays(180)) + { + return subscription.PeriodEndDate.Value; + } + + return expirationDate; + } + + public static T GetValue(this ClaimsPrincipal principal, string claimType) + { + var claim = principal.FindFirst(claimType); + + if (claim is null) + { + return default; + } + + // Handle Guid + if (typeof(T) == typeof(Guid)) + { + return Guid.TryParse(claim.Value, out var guid) + ? (T)(object)guid + : default; + } + + // Handle DateTime + if (typeof(T) == typeof(DateTime)) + { + return DateTime.TryParse(claim.Value, out var dateTime) + ? (T)(object)dateTime + : default; + } + + // Handle TimeSpan + if (typeof(T) == typeof(TimeSpan)) + { + return TimeSpan.TryParse(claim.Value, out var timeSpan) + ? (T)(object)timeSpan + : default; + } + + // Check for Nullable Types + var underlyingType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + + // Handle Enums + if (underlyingType.IsEnum) + { + if (Enum.TryParse(underlyingType, claim.Value, true, out var enumValue)) + { + return (T)enumValue; // Cast back to T + } + + return default; // Return default value for non-nullable enums or null for nullable enums + } + + // Handle other Nullable Types (e.g., int?, bool?) + if (underlyingType == typeof(int)) + { + return int.TryParse(claim.Value, out var intValue) + ? (T)(object)intValue + : default; + } + + if (underlyingType == typeof(bool)) + { + return bool.TryParse(claim.Value, out var boolValue) + ? (T)(object)boolValue + : default; + } + + if (underlyingType == typeof(double)) + { + return double.TryParse(claim.Value, out var doubleValue) + ? (T)(object)doubleValue + : default; + } + + // Fallback to Convert.ChangeType for other types including strings + return (T)Convert.ChangeType(claim.Value, underlyingType); + } +} diff --git a/src/Core/Billing/Licenses/Extensions/LicenseServiceCollectionExtensions.cs b/src/Core/Billing/Licenses/Extensions/LicenseServiceCollectionExtensions.cs new file mode 100644 index 0000000000..b08adbd004 --- /dev/null +++ b/src/Core/Billing/Licenses/Extensions/LicenseServiceCollectionExtensions.cs @@ -0,0 +1,16 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Licenses.Services; +using Bit.Core.Billing.Licenses.Services.Implementations; +using Bit.Core.Entities; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Billing.Licenses.Extensions; + +public static class LicenseServiceCollectionExtensions +{ + public static void AddLicenseServices(this IServiceCollection services) + { + services.AddTransient, OrganizationLicenseClaimsFactory>(); + services.AddTransient, UserLicenseClaimsFactory>(); + } +} diff --git a/src/Core/Billing/Licenses/LicenseConstants.cs b/src/Core/Billing/Licenses/LicenseConstants.cs new file mode 100644 index 0000000000..564019affc --- /dev/null +++ b/src/Core/Billing/Licenses/LicenseConstants.cs @@ -0,0 +1,58 @@ +namespace Bit.Core.Billing.Licenses; + +public static class OrganizationLicenseConstants +{ + public const string LicenseType = nameof(LicenseType); + public const string LicenseKey = nameof(LicenseKey); + public const string InstallationId = nameof(InstallationId); + public const string Id = nameof(Id); + public const string Name = nameof(Name); + public const string BusinessName = nameof(BusinessName); + public const string BillingEmail = nameof(BillingEmail); + public const string Enabled = nameof(Enabled); + public const string Plan = nameof(Plan); + public const string PlanType = nameof(PlanType); + public const string Seats = nameof(Seats); + public const string MaxCollections = nameof(MaxCollections); + public const string UsePolicies = nameof(UsePolicies); + public const string UseSso = nameof(UseSso); + public const string UseKeyConnector = nameof(UseKeyConnector); + public const string UseScim = nameof(UseScim); + public const string UseGroups = nameof(UseGroups); + public const string UseEvents = nameof(UseEvents); + public const string UseDirectory = nameof(UseDirectory); + public const string UseTotp = nameof(UseTotp); + public const string Use2fa = nameof(Use2fa); + public const string UseApi = nameof(UseApi); + public const string UseResetPassword = nameof(UseResetPassword); + public const string MaxStorageGb = nameof(MaxStorageGb); + public const string SelfHost = nameof(SelfHost); + public const string UsersGetPremium = nameof(UsersGetPremium); + public const string UseCustomPermissions = nameof(UseCustomPermissions); + public const string Issued = nameof(Issued); + public const string UsePasswordManager = nameof(UsePasswordManager); + public const string UseSecretsManager = nameof(UseSecretsManager); + public const string SmSeats = nameof(SmSeats); + public const string SmServiceAccounts = nameof(SmServiceAccounts); + public const string LimitCollectionCreationDeletion = nameof(LimitCollectionCreationDeletion); + public const string AllowAdminAccessToAllCollectionItems = nameof(AllowAdminAccessToAllCollectionItems); + public const string Expires = nameof(Expires); + public const string Refresh = nameof(Refresh); + public const string ExpirationWithoutGracePeriod = nameof(ExpirationWithoutGracePeriod); + public const string Trial = nameof(Trial); +} + +public static class UserLicenseConstants +{ + public const string LicenseType = nameof(LicenseType); + public const string LicenseKey = nameof(LicenseKey); + public const string Id = nameof(Id); + public const string Name = nameof(Name); + public const string Email = nameof(Email); + public const string Premium = nameof(Premium); + public const string MaxStorageGb = nameof(MaxStorageGb); + public const string Issued = nameof(Issued); + public const string Expires = nameof(Expires); + public const string Refresh = nameof(Refresh); + public const string Trial = nameof(Trial); +} diff --git a/src/Core/Billing/Licenses/Models/LicenseContext.cs b/src/Core/Billing/Licenses/Models/LicenseContext.cs new file mode 100644 index 0000000000..8dcc24e939 --- /dev/null +++ b/src/Core/Billing/Licenses/Models/LicenseContext.cs @@ -0,0 +1,10 @@ +#nullable enable +using Bit.Core.Models.Business; + +namespace Bit.Core.Billing.Licenses.Models; + +public class LicenseContext +{ + public Guid? InstallationId { get; init; } + public required SubscriptionInfo SubscriptionInfo { get; init; } +} diff --git a/src/Core/Billing/Licenses/Services/ILicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/ILicenseClaimsFactory.cs new file mode 100644 index 0000000000..926ad04683 --- /dev/null +++ b/src/Core/Billing/Licenses/Services/ILicenseClaimsFactory.cs @@ -0,0 +1,9 @@ +using System.Security.Claims; +using Bit.Core.Billing.Licenses.Models; + +namespace Bit.Core.Billing.Licenses.Services; + +public interface ILicenseClaimsFactory +{ + Task> GenerateClaims(T entity, LicenseContext licenseContext); +} diff --git a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs new file mode 100644 index 0000000000..300b87dcca --- /dev/null +++ b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs @@ -0,0 +1,75 @@ +using System.Globalization; +using System.Security.Claims; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Licenses.Extensions; +using Bit.Core.Billing.Licenses.Models; +using Bit.Core.Enums; +using Bit.Core.Models.Business; + +namespace Bit.Core.Billing.Licenses.Services.Implementations; + +public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory +{ + public Task> GenerateClaims(Organization entity, LicenseContext licenseContext) + { + var subscriptionInfo = licenseContext.SubscriptionInfo; + var expires = entity.CalculateFreshExpirationDate(subscriptionInfo); + var refresh = entity.CalculateFreshRefreshDate(subscriptionInfo, expires); + var expirationWithoutGracePeriod = entity.CalculateFreshExpirationDateWithoutGracePeriod(subscriptionInfo, expires); + var trial = IsTrialing(entity, subscriptionInfo); + + var claims = new List + { + new(nameof(OrganizationLicenseConstants.LicenseType), LicenseType.Organization.ToString()), + new Claim(nameof(OrganizationLicenseConstants.LicenseKey), entity.LicenseKey), + new(nameof(OrganizationLicenseConstants.InstallationId), licenseContext.InstallationId.ToString()), + new(nameof(OrganizationLicenseConstants.Id), entity.Id.ToString()), + new(nameof(OrganizationLicenseConstants.Name), entity.Name), + new(nameof(OrganizationLicenseConstants.BillingEmail), entity.BillingEmail), + new(nameof(OrganizationLicenseConstants.Enabled), entity.Enabled.ToString()), + new(nameof(OrganizationLicenseConstants.Plan), entity.Plan), + new(nameof(OrganizationLicenseConstants.PlanType), entity.PlanType.ToString()), + new(nameof(OrganizationLicenseConstants.Seats), entity.Seats.ToString()), + new(nameof(OrganizationLicenseConstants.MaxCollections), entity.MaxCollections.ToString()), + new(nameof(OrganizationLicenseConstants.UsePolicies), entity.UsePolicies.ToString()), + new(nameof(OrganizationLicenseConstants.UseSso), entity.UseSso.ToString()), + new(nameof(OrganizationLicenseConstants.UseKeyConnector), entity.UseKeyConnector.ToString()), + new(nameof(OrganizationLicenseConstants.UseScim), entity.UseScim.ToString()), + new(nameof(OrganizationLicenseConstants.UseGroups), entity.UseGroups.ToString()), + new(nameof(OrganizationLicenseConstants.UseEvents), entity.UseEvents.ToString()), + new(nameof(OrganizationLicenseConstants.UseDirectory), entity.UseDirectory.ToString()), + new(nameof(OrganizationLicenseConstants.UseTotp), entity.UseTotp.ToString()), + new(nameof(OrganizationLicenseConstants.Use2fa), entity.Use2fa.ToString()), + new(nameof(OrganizationLicenseConstants.UseApi), entity.UseApi.ToString()), + new(nameof(OrganizationLicenseConstants.UseResetPassword), entity.UseResetPassword.ToString()), + new(nameof(OrganizationLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString()), + new(nameof(OrganizationLicenseConstants.SelfHost), entity.SelfHost.ToString()), + new(nameof(OrganizationLicenseConstants.UsersGetPremium), entity.UsersGetPremium.ToString()), + new(nameof(OrganizationLicenseConstants.UseCustomPermissions), entity.UseCustomPermissions.ToString()), + new(nameof(OrganizationLicenseConstants.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)), + new(nameof(OrganizationLicenseConstants.UsePasswordManager), entity.UsePasswordManager.ToString()), + new(nameof(OrganizationLicenseConstants.UseSecretsManager), entity.UseSecretsManager.ToString()), + new(nameof(OrganizationLicenseConstants.SmSeats), entity.SmSeats.ToString()), + new(nameof(OrganizationLicenseConstants.SmServiceAccounts), entity.SmServiceAccounts.ToString()), + new(nameof(OrganizationLicenseConstants.LimitCollectionCreationDeletion), entity.LimitCollectionCreationDeletion.ToString()), + new(nameof(OrganizationLicenseConstants.AllowAdminAccessToAllCollectionItems), entity.AllowAdminAccessToAllCollectionItems.ToString()), + new(nameof(OrganizationLicenseConstants.Expires), expires.ToString(CultureInfo.InvariantCulture)), + new(nameof(OrganizationLicenseConstants.Refresh), refresh.ToString(CultureInfo.InvariantCulture)), + new(nameof(OrganizationLicenseConstants.ExpirationWithoutGracePeriod), expirationWithoutGracePeriod.ToString(CultureInfo.InvariantCulture)), + new(nameof(OrganizationLicenseConstants.Trial), trial.ToString()), + }; + + if (entity.BusinessName is not null) + { + claims.Add(new Claim(nameof(OrganizationLicenseConstants.BusinessName), entity.BusinessName)); + } + + return Task.FromResult(claims); + } + + private static bool IsTrialing(Organization org, SubscriptionInfo subscriptionInfo) => + subscriptionInfo?.Subscription is null + ? org.PlanType != PlanType.Custom || !org.ExpirationDate.HasValue + : subscriptionInfo.Subscription.TrialEndDate > DateTime.UtcNow; +} diff --git a/src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs new file mode 100644 index 0000000000..28c779c3d6 --- /dev/null +++ b/src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs @@ -0,0 +1,37 @@ +using System.Globalization; +using System.Security.Claims; +using Bit.Core.Billing.Licenses.Models; +using Bit.Core.Entities; +using Bit.Core.Enums; + +namespace Bit.Core.Billing.Licenses.Services.Implementations; + +public class UserLicenseClaimsFactory : ILicenseClaimsFactory +{ + public Task> GenerateClaims(User entity, LicenseContext licenseContext) + { + var subscriptionInfo = licenseContext.SubscriptionInfo; + + var expires = subscriptionInfo.UpcomingInvoice?.Date?.AddDays(7) ?? entity.PremiumExpirationDate?.AddDays(7); + var refresh = subscriptionInfo.UpcomingInvoice?.Date ?? entity.PremiumExpirationDate; + var trial = (subscriptionInfo.Subscription?.TrialEndDate.HasValue ?? false) && + subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow; + + var claims = new List + { + new(nameof(UserLicenseConstants.LicenseType), LicenseType.User.ToString()), + new(nameof(UserLicenseConstants.LicenseKey), entity.LicenseKey), + new(nameof(UserLicenseConstants.Id), entity.Id.ToString()), + new(nameof(UserLicenseConstants.Name), entity.Name), + new(nameof(UserLicenseConstants.Email), entity.Email), + new(nameof(UserLicenseConstants.Premium), entity.Premium.ToString()), + new(nameof(UserLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString()), + new(nameof(UserLicenseConstants.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)), + new(nameof(UserLicenseConstants.Expires), expires.ToString()), + new(nameof(UserLicenseConstants.Refresh), refresh.ToString()), + new(nameof(UserLicenseConstants.Trial), trial.ToString()), + }; + + return Task.FromResult(claims); + } +} diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 46ed985cb8..14258353d6 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -157,6 +157,7 @@ public static class FeatureFlagKeys public const string MacOsNativeCredentialSync = "macos-native-credential-sync"; public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; public const string InlineMenuTotp = "inline-menu-totp"; + public const string SelfHostLicenseRefactor = "pm-11516-self-host-license-refactor"; public static List GetAllKeys() { diff --git a/src/Core/Models/Business/ILicense.cs b/src/Core/Models/Business/ILicense.cs index ad389b0a12..b0e295bdd9 100644 --- a/src/Core/Models/Business/ILicense.cs +++ b/src/Core/Models/Business/ILicense.cs @@ -12,6 +12,7 @@ public interface ILicense bool Trial { get; set; } string Hash { get; set; } string Signature { get; set; } + string Token { get; set; } byte[] SignatureBytes { get; } byte[] GetDataBytes(bool forHash = false); byte[] ComputeHash(); diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs index ea51273645..37a086646c 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Models/Business/OrganizationLicense.cs @@ -1,10 +1,12 @@ using System.Reflection; +using System.Security.Claims; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json.Serialization; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Licenses.Extensions; using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Settings; @@ -151,6 +153,7 @@ public class OrganizationLicense : ILicense public LicenseType? LicenseType { get; set; } public string Hash { get; set; } public string Signature { get; set; } + public string Token { get; set; } [JsonIgnore] public byte[] SignatureBytes => Convert.FromBase64String(Signature); /// @@ -176,6 +179,7 @@ public class OrganizationLicense : ILicense !p.Name.Equals(nameof(Signature)) && !p.Name.Equals(nameof(SignatureBytes)) && !p.Name.Equals(nameof(LicenseType)) && + !p.Name.Equals(nameof(Token)) && // UsersGetPremium was added in Version 2 (Version >= 2 || !p.Name.Equals(nameof(UsersGetPremium))) && // UseEvents was added in Version 3 @@ -236,8 +240,65 @@ public class OrganizationLicense : ILicense } } - public bool CanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception) + public bool CanUse( + IGlobalSettings globalSettings, + ILicensingService licensingService, + ClaimsPrincipal claimsPrincipal, + out string exception) { + if (string.IsNullOrWhiteSpace(Token) || claimsPrincipal is null) + { + return ObsoleteCanUse(globalSettings, licensingService, out exception); + } + + var errorMessages = new StringBuilder(); + + var enabled = claimsPrincipal.GetValue(nameof(Enabled)); + if (!enabled) + { + errorMessages.AppendLine("Your cloud-hosted organization is currently disabled."); + } + + var installationId = claimsPrincipal.GetValue(nameof(InstallationId)); + if (installationId != globalSettings.Installation.Id) + { + errorMessages.AppendLine("The installation ID does not match the current installation."); + } + + var selfHost = claimsPrincipal.GetValue(nameof(SelfHost)); + if (!selfHost) + { + errorMessages.AppendLine("The license does not allow for on-premise hosting of organizations."); + } + + var licenseType = claimsPrincipal.GetValue(nameof(LicenseType)); + if (licenseType != Enums.LicenseType.Organization) + { + errorMessages.AppendLine("Premium licenses cannot be applied to an organization. " + + "Upload this license from your personal account settings page."); + } + + if (errorMessages.Length > 0) + { + exception = $"Invalid license. {errorMessages.ToString().TrimEnd()}"; + return false; + } + + exception = ""; + return true; + } + + /// + /// Do not extend this method. It is only here for backwards compatibility with old licenses. + /// Instead, extend the CanUse method using the ClaimsPrincipal. + /// + /// + /// + /// + /// + private bool ObsoleteCanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception) + { + // Do not extend this method. It is only here for backwards compatibility with old licenses. var errorMessages = new StringBuilder(); if (!Enabled) @@ -291,101 +352,177 @@ public class OrganizationLicense : ILicense return true; } - public bool VerifyData(Organization organization, IGlobalSettings globalSettings) + public bool VerifyData( + Organization organization, + ClaimsPrincipal claimsPrincipal, + IGlobalSettings globalSettings) { + if (string.IsNullOrWhiteSpace(Token)) + { + return ObsoleteVerifyData(organization, globalSettings); + } + + var issued = claimsPrincipal.GetValue(nameof(Issued)); + var expires = claimsPrincipal.GetValue(nameof(Expires)); + var installationId = claimsPrincipal.GetValue(nameof(InstallationId)); + var licenseKey = claimsPrincipal.GetValue(nameof(LicenseKey)); + var enabled = claimsPrincipal.GetValue(nameof(Enabled)); + var planType = claimsPrincipal.GetValue(nameof(PlanType)); + var seats = claimsPrincipal.GetValue(nameof(Seats)); + var maxCollections = claimsPrincipal.GetValue(nameof(MaxCollections)); + var useGroups = claimsPrincipal.GetValue(nameof(UseGroups)); + var useDirectory = claimsPrincipal.GetValue(nameof(UseDirectory)); + var useTotp = claimsPrincipal.GetValue(nameof(UseTotp)); + var selfHost = claimsPrincipal.GetValue(nameof(SelfHost)); + var name = claimsPrincipal.GetValue(nameof(Name)); + var usersGetPremium = claimsPrincipal.GetValue(nameof(UsersGetPremium)); + var useEvents = claimsPrincipal.GetValue(nameof(UseEvents)); + var use2fa = claimsPrincipal.GetValue(nameof(Use2fa)); + var useApi = claimsPrincipal.GetValue(nameof(UseApi)); + var usePolicies = claimsPrincipal.GetValue(nameof(UsePolicies)); + var useSso = claimsPrincipal.GetValue(nameof(UseSso)); + var useResetPassword = claimsPrincipal.GetValue(nameof(UseResetPassword)); + var useKeyConnector = claimsPrincipal.GetValue(nameof(UseKeyConnector)); + var useScim = claimsPrincipal.GetValue(nameof(UseScim)); + var useCustomPermissions = claimsPrincipal.GetValue(nameof(UseCustomPermissions)); + var useSecretsManager = claimsPrincipal.GetValue(nameof(UseSecretsManager)); + var usePasswordManager = claimsPrincipal.GetValue(nameof(UsePasswordManager)); + var smSeats = claimsPrincipal.GetValue(nameof(SmSeats)); + var smServiceAccounts = claimsPrincipal.GetValue(nameof(SmServiceAccounts)); + + return issued <= DateTime.UtcNow && + expires >= DateTime.UtcNow && + installationId == globalSettings.Installation.Id && + licenseKey == organization.LicenseKey && + enabled == organization.Enabled && + planType == organization.PlanType && + seats == organization.Seats && + maxCollections == organization.MaxCollections && + useGroups == organization.UseGroups && + useDirectory == organization.UseDirectory && + useTotp == organization.UseTotp && + selfHost == organization.SelfHost && + name == organization.Name && + usersGetPremium == organization.UsersGetPremium && + useEvents == organization.UseEvents && + use2fa == organization.Use2fa && + useApi == organization.UseApi && + usePolicies == organization.UsePolicies && + useSso == organization.UseSso && + useResetPassword == organization.UseResetPassword && + useKeyConnector == organization.UseKeyConnector && + useScim == organization.UseScim && + useCustomPermissions == organization.UseCustomPermissions && + useSecretsManager == organization.UseSecretsManager && + usePasswordManager == organization.UsePasswordManager && + smSeats == organization.SmSeats && + smServiceAccounts == organization.SmServiceAccounts; + } + + /// + /// Do not extend this method. It is only here for backwards compatibility with old licenses. + /// Instead, extend the VerifyData method using the ClaimsPrincipal. + /// + /// + /// + /// + /// + private bool ObsoleteVerifyData(Organization organization, IGlobalSettings globalSettings) + { + // Do not extend this method. It is only here for backwards compatibility with old licenses. if (Issued > DateTime.UtcNow || Expires < DateTime.UtcNow) { return false; } - if (ValidLicenseVersion) + if (!ValidLicenseVersion) { - var valid = - globalSettings.Installation.Id == InstallationId && - organization.LicenseKey != null && organization.LicenseKey.Equals(LicenseKey) && - organization.Enabled == Enabled && - organization.PlanType == PlanType && - organization.Seats == Seats && - organization.MaxCollections == MaxCollections && - organization.UseGroups == UseGroups && - organization.UseDirectory == UseDirectory && - organization.UseTotp == UseTotp && - organization.SelfHost == SelfHost && - organization.Name.Equals(Name); + throw new NotSupportedException($"Version {Version} is not supported."); + } - if (valid && Version >= 2) - { - valid = organization.UsersGetPremium == UsersGetPremium; - } + var valid = + globalSettings.Installation.Id == InstallationId && + organization.LicenseKey != null && organization.LicenseKey.Equals(LicenseKey) && + organization.Enabled == Enabled && + organization.PlanType == PlanType && + organization.Seats == Seats && + organization.MaxCollections == MaxCollections && + organization.UseGroups == UseGroups && + organization.UseDirectory == UseDirectory && + organization.UseTotp == UseTotp && + organization.SelfHost == SelfHost && + organization.Name.Equals(Name); - if (valid && Version >= 3) - { - valid = organization.UseEvents == UseEvents; - } + if (valid && Version >= 2) + { + valid = organization.UsersGetPremium == UsersGetPremium; + } - if (valid && Version >= 4) - { - valid = organization.Use2fa == Use2fa; - } + if (valid && Version >= 3) + { + valid = organization.UseEvents == UseEvents; + } - if (valid && Version >= 5) - { - valid = organization.UseApi == UseApi; - } + if (valid && Version >= 4) + { + valid = organization.Use2fa == Use2fa; + } - if (valid && Version >= 6) - { - valid = organization.UsePolicies == UsePolicies; - } + if (valid && Version >= 5) + { + valid = organization.UseApi == UseApi; + } - if (valid && Version >= 7) - { - valid = organization.UseSso == UseSso; - } + if (valid && Version >= 6) + { + valid = organization.UsePolicies == UsePolicies; + } - if (valid && Version >= 8) - { - valid = organization.UseResetPassword == UseResetPassword; - } + if (valid && Version >= 7) + { + valid = organization.UseSso == UseSso; + } - if (valid && Version >= 9) - { - valid = organization.UseKeyConnector == UseKeyConnector; - } + if (valid && Version >= 8) + { + valid = organization.UseResetPassword == UseResetPassword; + } - if (valid && Version >= 10) - { - valid = organization.UseScim == UseScim; - } + if (valid && Version >= 9) + { + valid = organization.UseKeyConnector == UseKeyConnector; + } - if (valid && Version >= 11) - { - valid = organization.UseCustomPermissions == UseCustomPermissions; - } + if (valid && Version >= 10) + { + valid = organization.UseScim == UseScim; + } - /*Version 12 added ExpirationWithoutDatePeriod, but that property is informational only and is not saved + if (valid && Version >= 11) + { + valid = organization.UseCustomPermissions == UseCustomPermissions; + } + + /*Version 12 added ExpirationWithoutDatePeriod, but that property is informational only and is not saved to the Organization object. It's validated as part of the hash but does not need to be validated here. */ - if (valid && Version >= 13) - { - valid = organization.UseSecretsManager == UseSecretsManager && - organization.UsePasswordManager == UsePasswordManager && - organization.SmSeats == SmSeats && - organization.SmServiceAccounts == SmServiceAccounts; - } + if (valid && Version >= 13) + { + valid = organization.UseSecretsManager == UseSecretsManager && + organization.UsePasswordManager == UsePasswordManager && + organization.SmSeats == SmSeats && + organization.SmServiceAccounts == SmServiceAccounts; + } - /* + /* * Version 14 added LimitCollectionCreationDeletion and Version * 15 added AllowAdminAccessToAllCollectionItems, however they * are no longer used and are intentionally excluded from * validation. */ - return valid; - } - - throw new NotSupportedException($"Version {Version} is not supported."); + return valid; } public bool VerifySignature(X509Certificate2 certificate) diff --git a/src/Core/Models/Business/UserLicense.cs b/src/Core/Models/Business/UserLicense.cs index 0f1b191a1d..797aa6692a 100644 --- a/src/Core/Models/Business/UserLicense.cs +++ b/src/Core/Models/Business/UserLicense.cs @@ -1,8 +1,10 @@ using System.Reflection; +using System.Security.Claims; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json.Serialization; +using Bit.Core.Billing.Licenses.Extensions; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Services; @@ -70,6 +72,7 @@ public class UserLicense : ILicense public LicenseType? LicenseType { get; set; } public string Hash { get; set; } public string Signature { get; set; } + public string Token { get; set; } [JsonIgnore] public byte[] SignatureBytes => Convert.FromBase64String(Signature); @@ -84,6 +87,7 @@ public class UserLicense : ILicense !p.Name.Equals(nameof(Signature)) && !p.Name.Equals(nameof(SignatureBytes)) && !p.Name.Equals(nameof(LicenseType)) && + !p.Name.Equals(nameof(Token)) && ( !forHash || ( @@ -113,8 +117,47 @@ public class UserLicense : ILicense } } - public bool CanUse(User user, out string exception) + public bool CanUse(User user, ClaimsPrincipal claimsPrincipal, out string exception) { + if (string.IsNullOrWhiteSpace(Token) || claimsPrincipal is null) + { + return ObsoleteCanUse(user, out exception); + } + + var errorMessages = new StringBuilder(); + + if (!user.EmailVerified) + { + errorMessages.AppendLine("The user's email is not verified."); + } + + var email = claimsPrincipal.GetValue(nameof(Email)); + if (!email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase)) + { + errorMessages.AppendLine("The user's email does not match the license email."); + } + + if (errorMessages.Length > 0) + { + exception = $"Invalid license. {errorMessages.ToString().TrimEnd()}"; + return false; + } + + exception = ""; + return true; + } + + /// + /// Do not extend this method. It is only here for backwards compatibility with old licenses. + /// Instead, extend the CanUse method using the ClaimsPrincipal. + /// + /// + /// + /// + /// + private bool ObsoleteCanUse(User user, out string exception) + { + // Do not extend this method. It is only here for backwards compatibility with old licenses. var errorMessages = new StringBuilder(); if (Issued > DateTime.UtcNow) @@ -152,22 +195,46 @@ public class UserLicense : ILicense return true; } - public bool VerifyData(User user) + public bool VerifyData(User user, ClaimsPrincipal claimsPrincipal) { + if (string.IsNullOrWhiteSpace(Token) || claimsPrincipal is null) + { + return ObsoleteVerifyData(user); + } + + var licenseKey = claimsPrincipal.GetValue(nameof(LicenseKey)); + var premium = claimsPrincipal.GetValue(nameof(Premium)); + var email = claimsPrincipal.GetValue(nameof(Email)); + + return licenseKey == user.LicenseKey && + premium == user.Premium && + email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase); + } + + /// + /// Do not extend this method. It is only here for backwards compatibility with old licenses. + /// Instead, extend the VerifyData method using the ClaimsPrincipal. + /// + /// + /// + /// + private bool ObsoleteVerifyData(User user) + { + // Do not extend this method. It is only here for backwards compatibility with old licenses. if (Issued > DateTime.UtcNow || Expires < DateTime.UtcNow) { return false; } - if (Version == 1) + if (Version != 1) { - return - user.LicenseKey != null && user.LicenseKey.Equals(LicenseKey) && - user.Premium == Premium && - user.Email.Equals(Email, StringComparison.InvariantCultureIgnoreCase); + throw new NotSupportedException($"Version {Version} is not supported."); } - throw new NotSupportedException($"Version {Version} is not supported."); + return + user.LicenseKey != null && user.LicenseKey.Equals(LicenseKey) && + user.Premium == Premium && + user.Email.Equals(Email, StringComparison.InvariantCultureIgnoreCase); } public bool VerifySignature(X509Certificate2 certificate) diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs b/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs index b8fad451e2..a4b08736c2 100644 --- a/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs +++ b/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs @@ -33,6 +33,10 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer } var subscriptionInfo = await _paymentService.GetSubscriptionAsync(organization); - return new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version); + + return new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version) + { + Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo) + }; } } diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs b/src/Core/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs index 1f8c6604b8..ffeee39c07 100644 --- a/src/Core/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs @@ -39,7 +39,8 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman throw new BadRequestException("License is already in use by another organization."); } - var canUse = license.CanUse(_globalSettings, _licensingService, out var exception) && + var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license); + var canUse = license.CanUse(_globalSettings, _licensingService, claimsPrincipal, out var exception) && selfHostedOrganization.CanUseLicense(license, out exception); if (!canUse) diff --git a/src/Core/Services/ILicensingService.cs b/src/Core/Services/ILicensingService.cs index e92fa87fd6..7301f7c689 100644 --- a/src/Core/Services/ILicensingService.cs +++ b/src/Core/Services/ILicensingService.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Entities; +using System.Security.Claims; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Entities; using Bit.Core.Models.Business; @@ -13,5 +14,12 @@ public interface ILicensingService byte[] SignLicense(ILicense license); Task ReadOrganizationLicenseAsync(Organization organization); Task ReadOrganizationLicenseAsync(Guid organizationId); + ClaimsPrincipal GetClaimsPrincipalFromLicense(ILicense license); + Task CreateOrganizationTokenAsync( + Organization organization, + Guid installationId, + SubscriptionInfo subscriptionInfo); + + Task CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo); } diff --git a/src/Core/Services/Implementations/LicensingService.cs b/src/Core/Services/Implementations/LicensingService.cs index 85b8f31200..866f0bb6e1 100644 --- a/src/Core/Services/Implementations/LicensingService.cs +++ b/src/Core/Services/Implementations/LicensingService.cs @@ -1,15 +1,22 @@ -using System.Security.Cryptography.X509Certificates; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json; using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Licenses.Models; +using Bit.Core.Billing.Licenses.Services; using Bit.Core.Entities; +using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Core.Utilities; +using IdentityModel; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; namespace Bit.Core.Services; @@ -19,27 +26,33 @@ public class LicensingService : ILicensingService private readonly IGlobalSettings _globalSettings; private readonly IUserRepository _userRepository; private readonly IOrganizationRepository _organizationRepository; - private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IMailService _mailService; private readonly ILogger _logger; + private readonly ILicenseClaimsFactory _organizationLicenseClaimsFactory; + private readonly ILicenseClaimsFactory _userLicenseClaimsFactory; + private readonly IFeatureService _featureService; private IDictionary _userCheckCache = new Dictionary(); public LicensingService( IUserRepository userRepository, IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, IMailService mailService, IWebHostEnvironment environment, ILogger logger, - IGlobalSettings globalSettings) + IGlobalSettings globalSettings, + ILicenseClaimsFactory organizationLicenseClaimsFactory, + IFeatureService featureService, + ILicenseClaimsFactory userLicenseClaimsFactory) { _userRepository = userRepository; _organizationRepository = organizationRepository; - _organizationUserRepository = organizationUserRepository; _mailService = mailService; _logger = logger; _globalSettings = globalSettings; + _organizationLicenseClaimsFactory = organizationLicenseClaimsFactory; + _featureService = featureService; + _userLicenseClaimsFactory = userLicenseClaimsFactory; var certThumbprint = environment.IsDevelopment() ? "207E64A231E8AA32AAF68A61037C075EBEBD553F" : @@ -104,13 +117,13 @@ public class LicensingService : ILicensingService continue; } - if (!license.VerifyData(org, _globalSettings)) + if (!license.VerifyData(org, GetClaimsPrincipalFromLicense(license), _globalSettings)) { await DisableOrganizationAsync(org, license, "Invalid data."); continue; } - if (!license.VerifySignature(_certificate)) + if (string.IsNullOrWhiteSpace(license.Token) && !license.VerifySignature(_certificate)) { await DisableOrganizationAsync(org, license, "Invalid signature."); continue; @@ -203,13 +216,14 @@ public class LicensingService : ILicensingService return false; } - if (!license.VerifyData(user)) + var claimsPrincipal = GetClaimsPrincipalFromLicense(license); + if (!license.VerifyData(user, claimsPrincipal)) { await DisablePremiumAsync(user, license, "Invalid data."); return false; } - if (!license.VerifySignature(_certificate)) + if (string.IsNullOrWhiteSpace(license.Token) && !license.VerifySignature(_certificate)) { await DisablePremiumAsync(user, license, "Invalid signature."); return false; @@ -234,7 +248,21 @@ public class LicensingService : ILicensingService public bool VerifyLicense(ILicense license) { - return license.VerifySignature(_certificate); + if (string.IsNullOrWhiteSpace(license.Token)) + { + return license.VerifySignature(_certificate); + } + + try + { + _ = GetClaimsPrincipalFromLicense(license); + return true; + } + catch (Exception e) + { + _logger.LogWarning(e, "Invalid token."); + return false; + } } public byte[] SignLicense(ILicense license) @@ -272,4 +300,101 @@ public class LicensingService : ILicensingService using var fs = File.OpenRead(filePath); return await JsonSerializer.DeserializeAsync(fs); } + + public ClaimsPrincipal GetClaimsPrincipalFromLicense(ILicense license) + { + if (string.IsNullOrWhiteSpace(license.Token)) + { + return null; + } + + var audience = license switch + { + OrganizationLicense orgLicense => $"organization:{orgLicense.Id}", + UserLicense userLicense => $"user:{userLicense.Id}", + _ => throw new ArgumentException("Unsupported license type.", nameof(license)), + }; + + var token = license.Token; + var tokenHandler = new JwtSecurityTokenHandler(); + var validationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new X509SecurityKey(_certificate), + ValidateIssuer = true, + ValidIssuer = "bitwarden", + ValidateAudience = true, + ValidAudience = audience, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero, + RequireExpirationTime = true + }; + + try + { + return tokenHandler.ValidateToken(token, validationParameters, out _); + } + catch (Exception ex) + { + // Token exceptions thrown are interpreted by the client as Identity errors and cause the user to logout + // Mask them by rethrowing as BadRequestException + throw new BadRequestException($"Invalid license. {ex.Message}"); + } + } + + public async Task CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo) + { + if (!_featureService.IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor)) + { + return null; + } + + var licenseContext = new LicenseContext + { + InstallationId = installationId, + SubscriptionInfo = subscriptionInfo, + }; + + var claims = await _organizationLicenseClaimsFactory.GenerateClaims(organization, licenseContext); + var audience = $"organization:{organization.Id}"; + + return GenerateToken(claims, audience); + } + + public async Task CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo) + { + if (!_featureService.IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor)) + { + return null; + } + + var licenseContext = new LicenseContext { SubscriptionInfo = subscriptionInfo }; + var claims = await _userLicenseClaimsFactory.GenerateClaims(user, licenseContext); + var audience = $"user:{user.Id}"; + + return GenerateToken(claims, audience); + } + + private string GenerateToken(List claims, string audience) + { + if (claims.All(claim => claim.Type != JwtClaimTypes.JwtId)) + { + claims.Add(new Claim(JwtClaimTypes.JwtId, Guid.NewGuid().ToString())); + } + + var securityKey = new RsaSecurityKey(_certificate.GetRSAPrivateKey()); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims), + Issuer = "bitwarden", + Audience = audience, + NotBefore = DateTime.UtcNow, + Expires = DateTime.UtcNow.AddYears(1), // Org expiration is a claim + SigningCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256Signature) + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + var token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 2199d0a7af..fa8cd3cef8 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -908,7 +908,9 @@ public class UserService : UserManager, IUserService, IDisposable throw new BadRequestException("Invalid license."); } - if (!license.CanUse(user, out var exceptionMessage)) + var claimsPrincipal = _licenseService.GetClaimsPrincipalFromLicense(license); + + if (!license.CanUse(user, claimsPrincipal, out var exceptionMessage)) { throw new BadRequestException(exceptionMessage); } @@ -987,7 +989,9 @@ public class UserService : UserManager, IUserService, IDisposable throw new BadRequestException("Invalid license."); } - if (!license.CanUse(user, out var exceptionMessage)) + var claimsPrincipal = _licenseService.GetClaimsPrincipalFromLicense(license); + + if (!license.CanUse(user, claimsPrincipal, out var exceptionMessage)) { throw new BadRequestException(exceptionMessage); } @@ -1111,7 +1115,9 @@ public class UserService : UserManager, IUserService, IDisposable } } - public async Task GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null, + public async Task GenerateLicenseAsync( + User user, + SubscriptionInfo subscriptionInfo = null, int? version = null) { if (user == null) @@ -1124,8 +1130,13 @@ public class UserService : UserManager, IUserService, IDisposable subscriptionInfo = await _paymentService.GetSubscriptionAsync(user); } - return subscriptionInfo == null ? new UserLicense(user, _licenseService) : - new UserLicense(user, subscriptionInfo, _licenseService); + var userLicense = subscriptionInfo == null + ? new UserLicense(user, _licenseService) + : new UserLicense(user, subscriptionInfo, _licenseService); + + userLicense.Token = await _licenseService.CreateUserTokenAsync(user, subscriptionInfo); + + return userLicense; } public override async Task CheckPasswordAsync(User user, string password) diff --git a/src/Core/Services/NoopImplementations/NoopLicensingService.cs b/src/Core/Services/NoopImplementations/NoopLicensingService.cs index 8eb42a318c..dc733e9a33 100644 --- a/src/Core/Services/NoopImplementations/NoopLicensingService.cs +++ b/src/Core/Services/NoopImplementations/NoopLicensingService.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Entities; +using System.Security.Claims; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Entities; using Bit.Core.Models.Business; using Bit.Core.Settings; @@ -53,4 +54,19 @@ public class NoopLicensingService : ILicensingService { return Task.FromResult(null); } + + public ClaimsPrincipal GetClaimsPrincipalFromLicense(ILicense license) + { + return null; + } + + public Task CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo) + { + return Task.FromResult(null); + } + + public Task CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo) + { + return Task.FromResult(null); + } } diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index ab4108bfef..7585739d82 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -230,7 +230,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddScoped(); services.AddSingleton(_ => { var options = new LookupClientOptions { Timeout = TimeSpan.FromSeconds(15), UseTcpOnly = true }; diff --git a/test/Core.Test/Models/Business/OrganizationLicenseTests.cs b/test/Core.Test/Models/Business/OrganizationLicenseTests.cs index c2eb0dd934..26945f533e 100644 --- a/test/Core.Test/Models/Business/OrganizationLicenseTests.cs +++ b/test/Core.Test/Models/Business/OrganizationLicenseTests.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Security.Claims; +using System.Text.Json; using Bit.Core.Models.Business; using Bit.Core.Services; using Bit.Core.Settings; @@ -36,7 +37,7 @@ public class OrganizationLicenseTests [Theory] [BitAutoData(OrganizationLicense.CurrentLicenseFileVersion)] // Previous version (this property is 1 behind) [BitAutoData(OrganizationLicense.CurrentLicenseFileVersion + 1)] // Current version - public void OrganizationLicense_LoadedFromDisk_VerifyData_Passes(int licenseVersion) + public void OrganizationLicense_LoadedFromDisk_VerifyData_Passes(int licenseVersion, ClaimsPrincipal claimsPrincipal) { var license = OrganizationLicenseFileFixtures.GetVersion(licenseVersion); @@ -49,7 +50,7 @@ public class OrganizationLicenseTests { Id = new Guid(OrganizationLicenseFileFixtures.InstallationId) }); - Assert.True(license.VerifyData(organization, globalSettings)); + Assert.True(license.VerifyData(organization, claimsPrincipal, globalSettings)); } /// diff --git a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs index 565f2f32c4..b8e677177c 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Entities; +using System.Security.Claims; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations; @@ -48,6 +49,9 @@ public class UpdateOrganizationLicenseCommandTests license.InstallationId = globalSettings.Installation.Id; license.LicenseType = LicenseType.Organization; sutProvider.GetDependency().VerifyLicense(license).Returns(true); + sutProvider.GetDependency() + .GetClaimsPrincipalFromLicense(license) + .Returns((ClaimsPrincipal)null); // Passing values for SelfHostedOrganizationDetails.CanUseLicense // NSubstitute cannot override non-virtual members so we have to ensure the real method passes @@ -79,10 +83,11 @@ public class UpdateOrganizationLicenseCommandTests .Received(1) .ReplaceAndUpdateCacheAsync(Arg.Is( org => AssertPropertyEqual(license, org, - "Id", "MaxStorageGb", "Issued", "Refresh", "Version", "Trial", "LicenseType", - "Hash", "Signature", "SignatureBytes", "InstallationId", "Expires", "ExpirationWithoutGracePeriod") && - // Same property but different name, use explicit mapping - org.ExpirationDate == license.Expires)); + "Id", "MaxStorageGb", "Issued", "Refresh", "Version", "Trial", "LicenseType", + "Hash", "Signature", "SignatureBytes", "InstallationId", "Expires", + "ExpirationWithoutGracePeriod", "Token") && + // Same property but different name, use explicit mapping + org.ExpirationDate == license.Expires)); } finally { diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index aa2c0a5cc9..71cceb86ad 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Security.Claims; +using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; @@ -62,6 +63,9 @@ public class UserServiceTests sutProvider.GetDependency() .VerifyLicense(userLicense) .Returns(true); + sutProvider.GetDependency() + .GetClaimsPrincipalFromLicense(userLicense) + .Returns((ClaimsPrincipal)null); await sutProvider.Sut.UpdateLicenseAsync(user, userLicense); From 04f9d7dd8e952d0820cbf3a0784b398fd36562f7 Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Thu, 5 Dec 2024 09:40:55 -0500 Subject: [PATCH 36/94] Remove SM team from CODEOWNERS (#5117) --- .github/CODEOWNERS | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 47d3525def..1d142016f2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -38,7 +38,6 @@ src/Identity @bitwarden/team-auth-dev # Key Management team **/KeyManagement @bitwarden/team-key-management-dev -**/SecretsManager @bitwarden/team-secrets-manager-dev **/Tools @bitwarden/team-tools-dev # Vault team From f471fffe42a2a5c73448c720f36255354091a1ea Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Thu, 5 Dec 2024 08:59:35 -0600 Subject: [PATCH 37/94] [PM-10317] Email Users For Org Claiming Domain (#5094) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revoking users when enabling single org and 2fa policies. Fixing tests. * Added migration. * Wrote tests and fixed bugs found. * Patch build process * Fixing tests. * Added unit test around disabling the feature flag. * Updated error message to be public and added test for validating the request. * formatting * Added some tests for single org policy validator. * Fix issues from merge. * Added sending emails to revoked non-compliant users. * Fixing name. Adding two factor policy email. * Send email when user has been revoked. * Correcting migration name. * Fixing templates and logic issue in Revoke command. * Moving interface into its own file. * Correcting namespaces for email templates. * correcting logic that would not allow normal users to revoke non owners. * Actually correcting the test and logic. * dotnet format. Added exec to bottom of bulk sproc * Update src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> * Updated OrgIds to be a json string * Fixing errors. * Updating test * Moving command result. * Formatting and request rename * Realized this would throw a null error from the system domain verification. Adding unknown type to event system user. Adding optional parameter to SaveAsync in policy service in order to pass in event system user. * Code review changes * Removing todos * Corrected test name. * Syncing filename to record name. * Fixing up the tests. * Added happy path test * Naming corrections. And corrected EF query. * added check against event service * Code review changes. * Fixing tests. * splitting up tests * Added templates and email side effect for claiming a domain. * bringing changes from nc user changes. * Switched to enqueue mail message. * Filled in DomainClaimedByOrganization.html.hbs * Added text document for domain claiming * Fixing migration script. * Remove old sproc * Limiting sending of the email down to users who are a part of the domain being claimed. * Added test for change * Renames and fixed up email. * Fixing up CSS --------- Co-authored-by: Matt Bishop Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> Co-authored-by: Rui Tome --- .../VerifyOrganizationDomainCommand.cs | 36 ++++++++++--- .../DomainClaimedByOrganization.html.hbs | 24 +++++++++ .../DomainClaimedByOrganization.text.hbs | 8 +++ .../ManagedUserDomainClaimedEmails.cs | 5 ++ .../ClaimedDomainUserNotificationViewModel.cs | 6 +++ src/Core/Services/IMailService.cs | 2 + .../Implementations/HandlebarsMailService.cs | 17 ++++++ .../NoopImplementations/NoopMailService.cs | 2 + .../VerifyOrganizationDomainCommandTests.cs | 54 ++++++++++++++++++- 9 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.text.hbs create mode 100644 src/Core/Models/Data/Organizations/ManagedUserDomainClaimedEmails.cs create mode 100644 src/Core/Models/Mail/ClaimedDomainUserNotificationViewModel.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs index 375c6326ef..e011819f0f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs @@ -7,6 +7,7 @@ using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -22,11 +23,12 @@ public class VerifyOrganizationDomainCommand( IFeatureService featureService, ICurrentContext currentContext, ISavePolicyCommand savePolicyCommand, + IMailService mailService, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, ILogger logger) : IVerifyOrganizationDomainCommand { - - public async Task UserVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain) { if (currentContext.UserId is null) @@ -109,7 +111,7 @@ public class VerifyOrganizationDomainCommand( { domain.SetVerifiedDate(); - await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser); + await DomainVerificationSideEffectsAsync(domain, actingUser); } } catch (Exception e) @@ -121,19 +123,37 @@ public class VerifyOrganizationDomainCommand( return domain; } - private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser) + private async Task DomainVerificationSideEffectsAsync(OrganizationDomain domain, IActingUser actingUser) { if (featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) { - var policyUpdate = new PolicyUpdate + await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser); + await SendVerifiedDomainUserEmailAsync(domain); + } + } + + private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser) => + await savePolicyCommand.SaveAsync( + new PolicyUpdate { OrganizationId = organizationId, Type = PolicyType.SingleOrg, Enabled = true, PerformedBy = actingUser - }; + }); - await savePolicyCommand.SaveAsync(policyUpdate); - } + private async Task SendVerifiedDomainUserEmailAsync(OrganizationDomain domain) + { + var orgUserUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync(domain.OrganizationId); + + var domainUserEmails = orgUserUsers + .Where(ou => ou.Email.ToLower().EndsWith($"@{domain.DomainName.ToLower()}") && + ou.Status != OrganizationUserStatusType.Revoked && + ou.Status != OrganizationUserStatusType.Invited) + .Select(ou => ou.Email); + + var organization = await organizationRepository.GetByIdAsync(domain.OrganizationId); + + await mailService.SendClaimedDomainUserEmailAsync(new ManagedUserDomainClaimedEmails(domainUserEmails, organization)); } } diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.html.hbs new file mode 100644 index 0000000000..05ca170a50 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.html.hbs @@ -0,0 +1,24 @@ +{{#>TitleContactUsHtmlLayout}} + + + + + + + + + + +
+ As a member of {{OrganizationName}}, your Bitwarden account is claimed and owned by your organization. +
+ Here's what that means: +
    +
  • This account should only be used to store items related to {{OrganizationName}}
  • +
  • Admins managing your Bitwarden organization manage your email address and other account settings
  • +
  • Admins can also revoke or delete your account at any time
  • +
+
+ For more information, please refer to the following help article: Claimed Accounts +
+{{/TitleContactUsHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.text.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.text.hbs new file mode 100644 index 0000000000..c0078d389d --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.text.hbs @@ -0,0 +1,8 @@ +As a member of {{OrganizationName}}, your Bitwarden account is claimed and owned by your organization. + +Here's what that means: +- This account should only be used to store items related to {{OrganizationName}} +- Your admins managing your Bitwarden organization manages your email address and other account settings +- Your admins can also revoke or delete your account at any time + +For more information, please refer to the following help article: Claimed Accounts (https://bitwarden.com/help/claimed-accounts) diff --git a/src/Core/Models/Data/Organizations/ManagedUserDomainClaimedEmails.cs b/src/Core/Models/Data/Organizations/ManagedUserDomainClaimedEmails.cs new file mode 100644 index 0000000000..429257e266 --- /dev/null +++ b/src/Core/Models/Data/Organizations/ManagedUserDomainClaimedEmails.cs @@ -0,0 +1,5 @@ +using Bit.Core.AdminConsole.Entities; + +namespace Bit.Core.Models.Data.Organizations; + +public record ManagedUserDomainClaimedEmails(IEnumerable EmailList, Organization Organization); diff --git a/src/Core/Models/Mail/ClaimedDomainUserNotificationViewModel.cs b/src/Core/Models/Mail/ClaimedDomainUserNotificationViewModel.cs new file mode 100644 index 0000000000..97591b51bc --- /dev/null +++ b/src/Core/Models/Mail/ClaimedDomainUserNotificationViewModel.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Models.Mail; + +public class ClaimedDomainUserNotificationViewModel : BaseTitleContactUsMailModel +{ + public string OrganizationName { get; init; } +} diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index bc8d1440f1..c6c9dc7948 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Auth.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Mail; namespace Bit.Core.Services; @@ -93,5 +94,6 @@ public interface IMailService Task SendRequestSMAccessToAdminEmailAsync(IEnumerable adminEmails, string organizationName, string userRequestingAccess, string emailContent); Task SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(string email, string offerAcceptanceDate, string organizationId, string organizationName); + Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList); } diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index acc729e53c..c220df18a1 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -7,6 +7,7 @@ using Bit.Core.Auth.Models.Mail; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.Mail; using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Mail; using Bit.Core.Models.Mail.FamiliesForEnterprise; using Bit.Core.Models.Mail.Provider; @@ -460,6 +461,22 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList) + { + await EnqueueMailAsync(emailList.EmailList.Select(email => + CreateMessage(email, emailList.Organization))); + return; + + MailQueueMessage CreateMessage(string emailAddress, Organization org) => + new(CreateDefaultMessage($"Your Bitwarden account is claimed by {org.DisplayName()}", emailAddress), + "AdminConsole.DomainClaimedByOrganization", + new ClaimedDomainUserNotificationViewModel + { + TitleFirst = $"Hey {emailAddress}, here is a heads up on your claimed account:", + OrganizationName = CoreHelpers.SanitizeForEmail(org.DisplayName(), false) + }); + } + public async Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip) { var message = CreateDefaultMessage($"New Device Logged In From {deviceType}", email); diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 399874eee7..e8ea8d9863 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Auth.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Mail; namespace Bit.Core.Services; @@ -309,5 +310,6 @@ public class NoopMailService : IMailService { return Task.FromResult(0); } + public Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList) => Task.CompletedTask; } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs index 700df88d54..6c6d0e35f0 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; @@ -7,6 +8,8 @@ using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; @@ -269,4 +272,53 @@ public class VerifyOrganizationDomainCommandTests .DidNotReceive() .SaveAsync(Arg.Any()); } + + [Theory, BitAutoData] + public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsVerified_ThenEmailShouldBeSentToUsersWhoBelongToTheDomain( + ICollection organizationUsers, + OrganizationDomain domain, + Organization organization, + SutProvider sutProvider) + { + foreach (var organizationUser in organizationUsers) + { + organizationUser.Email = $"{organizationUser.Name}@{domain.DomainName}"; + } + + var mockedUsers = organizationUsers + .Where(x => x.Status != OrganizationUserStatusType.Invited && + x.Status != OrganizationUserStatusType.Revoked).ToList(); + + organization.Id = domain.OrganizationId; + + sutProvider.GetDependency() + .GetClaimedDomainsByDomainNameAsync(domain.DomainName) + .Returns([]); + + sutProvider.GetDependency() + .GetByIdAsync(domain.OrganizationId) + .Returns(organization); + + sutProvider.GetDependency() + .ResolveAsync(domain.DomainName, domain.Txt) + .Returns(true); + + sutProvider.GetDependency() + .UserId.Returns(Guid.NewGuid()); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(domain.OrganizationId) + .Returns(mockedUsers); + + _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); + + await sutProvider.GetDependency().Received().SendClaimedDomainUserEmailAsync( + Arg.Is(x => + x.EmailList.Count(e => e.EndsWith(domain.DomainName)) == mockedUsers.Count && + x.Organization.Id == organization.Id)); + } } From 1f1510f4d48f68adb9c6f1babba8bef44293bfee Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Thu, 5 Dec 2024 10:46:01 -0600 Subject: [PATCH 38/94] PM-15091 Add Feature Flag to DB called UseRiskInsights (#5088) Add a new column called UseRiskInsights to `dbo.Organization` --- .../Controllers/OrganizationsController.cs | 1 + .../Models/OrganizationEditModel.cs | 4 + .../Models/OrganizationViewModel.cs | 1 + .../Views/Shared/_OrganizationForm.cshtml | 13 +- .../OrganizationResponseModel.cs | 2 + .../ProfileOrganizationResponseModel.cs | 2 + ...rofileProviderOrganizationResponseModel.cs | 1 + .../AdminConsole/Entities/Organization.cs | 5 + .../Data/Organizations/OrganizationAbility.cs | 2 + .../OrganizationUserOrganizationDetails.cs | 1 + .../ProviderUserOrganizationDetails.cs | 1 + .../Repositories/OrganizationRepository.cs | 3 +- ...izationUserOrganizationDetailsViewQuery.cs | 1 + ...roviderUserOrganizationDetailsViewQuery.cs | 1 + .../Stored Procedures/Organization_Create.sql | 9 +- .../Organization_ReadAbilities.sql | 3 +- .../Stored Procedures/Organization_Update.sql | 6 +- src/Sql/dbo/Tables/Organization.sql | 1 + ...rganizationUserOrganizationDetailsView.sql | 3 +- ...derUserProviderOrganizationDetailsView.sql | 3 +- .../OrganizationUserRepositoryTests.cs | 1 + ...25_00_AddUseRiskInsightsToOrganization.sql | 356 ++ ...024-11-25_01_AddUseRiskInsightsToViews.sql | 142 + ...5185627_AddUseRiskInsightsFlag.Designer.cs | 2943 ++++++++++++++++ .../20241125185627_AddUseRiskInsightsFlag.cs | 28 + .../DatabaseContextModelSnapshot.cs | 3 + ...5185635_AddUseRiskInsightsFlag.Designer.cs | 2949 +++++++++++++++++ .../20241125185635_AddUseRiskInsightsFlag.cs | 28 + .../DatabaseContextModelSnapshot.cs | 3 + ...5185632_AddUseRiskInsightsFlag.Designer.cs | 2932 ++++++++++++++++ .../20241125185632_AddUseRiskInsightsFlag.cs | 28 + .../DatabaseContextModelSnapshot.cs | 3 + 32 files changed, 9467 insertions(+), 12 deletions(-) create mode 100644 util/Migrator/DbScripts/2024-11-25_00_AddUseRiskInsightsToOrganization.sql create mode 100644 util/Migrator/DbScripts/2024-11-25_01_AddUseRiskInsightsToViews.sql create mode 100644 util/MySqlMigrations/Migrations/20241125185627_AddUseRiskInsightsFlag.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20241125185627_AddUseRiskInsightsFlag.cs create mode 100644 util/PostgresMigrations/Migrations/20241125185635_AddUseRiskInsightsFlag.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20241125185635_AddUseRiskInsightsFlag.cs create mode 100644 util/SqliteMigrations/Migrations/20241125185632_AddUseRiskInsightsFlag.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20241125185632_AddUseRiskInsightsFlag.cs diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index 67a80a3754..4c4df3d15b 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -448,6 +448,7 @@ public class OrganizationsController : Controller organization.UseTotp = model.UseTotp; organization.UsersGetPremium = model.UsersGetPremium; organization.UseSecretsManager = model.UseSecretsManager; + organization.UseRiskInsights = model.UseRiskInsights; //secrets organization.SmSeats = model.SmSeats; diff --git a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs index 48340df708..be191ddb8d 100644 --- a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs @@ -80,6 +80,7 @@ public class OrganizationEditModel : OrganizationViewModel Use2fa = org.Use2fa; UseApi = org.UseApi; UseSecretsManager = org.UseSecretsManager; + UseRiskInsights = org.UseRiskInsights; UseResetPassword = org.UseResetPassword; SelfHost = org.SelfHost; UsersGetPremium = org.UsersGetPremium; @@ -144,6 +145,8 @@ public class OrganizationEditModel : OrganizationViewModel public bool UseScim { get; set; } [Display(Name = "Secrets Manager")] public new bool UseSecretsManager { get; set; } + [Display(Name = "Risk Insights")] + public new bool UseRiskInsights { get; set; } [Display(Name = "Self Host")] public bool SelfHost { get; set; } [Display(Name = "Users Get Premium")] @@ -284,6 +287,7 @@ public class OrganizationEditModel : OrganizationViewModel existingOrganization.Use2fa = Use2fa; existingOrganization.UseApi = UseApi; existingOrganization.UseSecretsManager = UseSecretsManager; + existingOrganization.UseRiskInsights = UseRiskInsights; existingOrganization.UseResetPassword = UseResetPassword; existingOrganization.SelfHost = SelfHost; existingOrganization.UsersGetPremium = UsersGetPremium; diff --git a/src/Admin/AdminConsole/Models/OrganizationViewModel.cs b/src/Admin/AdminConsole/Models/OrganizationViewModel.cs index b58d3aa52e..69486bdcd2 100644 --- a/src/Admin/AdminConsole/Models/OrganizationViewModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationViewModel.cs @@ -69,4 +69,5 @@ public class OrganizationViewModel public int ServiceAccountsCount { get; set; } public int OccupiedSmSeatsCount { get; set; } public bool UseSecretsManager => Organization.UseSecretsManager; + public bool UseRiskInsights => Organization.UseRiskInsights; } diff --git a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml index 5187b6690a..23d2057d07 100644 --- a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml +++ b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml @@ -94,7 +94,7 @@

Features

-
+

General

@@ -146,7 +146,7 @@
-
+

Password Manager

@@ -157,13 +157,20 @@
-
+

Secrets Manager

+
+

Access Insights

+
+ + +
+
} diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs index 7808b564a8..908a3a9385 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs @@ -60,6 +60,7 @@ public class OrganizationResponseModel : ResponseModel // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; + UseRiskInsights = organization.UseRiskInsights; } public Guid Id { get; set; } @@ -106,6 +107,7 @@ public class OrganizationResponseModel : ResponseModel // Deperectated: https://bitwarden.atlassian.net/browse/PM-10863 public bool LimitCollectionCreationDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } + public bool UseRiskInsights { get; set; } } public class OrganizationSubscriptionResponseModel : OrganizationResponseModel diff --git a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs index 1fcaba5f93..96b86de164 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs @@ -71,6 +71,7 @@ public class ProfileOrganizationResponseModel : ResponseModel LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId); + UseRiskInsights = organization.UseRiskInsights; if (organization.SsoConfig != null) { @@ -143,4 +144,5 @@ public class ProfileOrganizationResponseModel : ResponseModel /// False if the Account Deprovisioning feature flag is disabled. /// public bool UserIsManagedByOrganization { get; set; } + public bool UseRiskInsights { get; set; } } diff --git a/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs index 92498834db..4ec86a29a4 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs @@ -49,5 +49,6 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo // https://bitwarden.atlassian.net/browse/PM-10863 LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; + UseRiskInsights = organization.UseRiskInsights; } } diff --git a/src/Core/AdminConsole/Entities/Organization.cs b/src/Core/AdminConsole/Entities/Organization.cs index c556dfe601..720a77d6a4 100644 --- a/src/Core/AdminConsole/Entities/Organization.cs +++ b/src/Core/AdminConsole/Entities/Organization.cs @@ -115,6 +115,11 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable, ///
public bool AllowAdminAccessToAllCollectionItems { get; set; } + /// + /// Risk Insights is a reporting feature that provides insights into the security of an organization's vault. + /// + public bool UseRiskInsights { get; set; } + public void SetNewId() { if (Id == default(Guid)) diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs index a91b960839..0da0928dbe 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs @@ -26,6 +26,7 @@ public class OrganizationAbility // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; + UseRiskInsights = organization.UseRiskInsights; } public Guid Id { get; set; } @@ -45,4 +46,5 @@ public class OrganizationAbility // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 public bool LimitCollectionCreationDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } + public bool UseRiskInsights { get; set; } } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs index 435369e77a..7b9ea971d3 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs @@ -59,4 +59,5 @@ public class OrganizationUserOrganizationDetails // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 public bool LimitCollectionCreationDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } + public bool UseRiskInsights { get; set; } } diff --git a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs index a2ac622539..c2880b543f 100644 --- a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs @@ -44,4 +44,5 @@ public class ProviderUserOrganizationDetails public bool LimitCollectionDeletion { get; set; } public bool LimitCollectionCreationDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } + public bool UseRiskInsights { get; set; } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index b3ee254889..7a667db8f5 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -103,7 +103,8 @@ public class OrganizationRepository : Repository +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("20241125185627_AddUseRiskInsightsFlag")] + partial class AddUseRiskInsightsFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionCreationDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseRiskInsights") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasColumnType("longtext"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AaGuid") + .HasColumnType("char(36)"); + + b.Property("Counter") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasColumnType("longtext"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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.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.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + 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/20241125185627_AddUseRiskInsightsFlag.cs b/util/MySqlMigrations/Migrations/20241125185627_AddUseRiskInsightsFlag.cs new file mode 100644 index 0000000000..7036c9aaae --- /dev/null +++ b/util/MySqlMigrations/Migrations/20241125185627_AddUseRiskInsightsFlag.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddUseRiskInsightsFlag : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UseRiskInsights", + table: "Organization", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UseRiskInsights", + table: "Organization"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 36c46f629f..5927762791 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -191,6 +191,9 @@ namespace Bit.MySqlMigrations.Migrations b.Property("UseResetPassword") .HasColumnType("tinyint(1)"); + b.Property("UseRiskInsights") + .HasColumnType("tinyint(1)"); + b.Property("UseScim") .HasColumnType("tinyint(1)"); diff --git a/util/PostgresMigrations/Migrations/20241125185635_AddUseRiskInsightsFlag.Designer.cs b/util/PostgresMigrations/Migrations/20241125185635_AddUseRiskInsightsFlag.Designer.cs new file mode 100644 index 0000000000..895a4765d8 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20241125185635_AddUseRiskInsightsFlag.Designer.cs @@ -0,0 +1,2949 @@ +// +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("20241125185635_AddUseRiskInsightsFlag")] + partial class AddUseRiskInsightsFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionCreationDeletion") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseRiskInsights") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasColumnType("text"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MasterPasswordHash") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("OrganizationId", "ExternalId"), new[] { "UserId" }); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("Counter") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsPrf") + .HasColumnType("boolean"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasColumnType("text"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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.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.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + 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/20241125185635_AddUseRiskInsightsFlag.cs b/util/PostgresMigrations/Migrations/20241125185635_AddUseRiskInsightsFlag.cs new file mode 100644 index 0000000000..36d7c77e44 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20241125185635_AddUseRiskInsightsFlag.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddUseRiskInsightsFlag : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UseRiskInsights", + table: "Organization", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UseRiskInsights", + table: "Organization"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 69c9dae160..4259d1aed8 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -193,6 +193,9 @@ namespace Bit.PostgresMigrations.Migrations b.Property("UseResetPassword") .HasColumnType("boolean"); + b.Property("UseRiskInsights") + .HasColumnType("boolean"); + b.Property("UseScim") .HasColumnType("boolean"); diff --git a/util/SqliteMigrations/Migrations/20241125185632_AddUseRiskInsightsFlag.Designer.cs b/util/SqliteMigrations/Migrations/20241125185632_AddUseRiskInsightsFlag.Designer.cs new file mode 100644 index 0000000000..9120ba9715 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20241125185632_AddUseRiskInsightsFlag.Designer.cs @@ -0,0 +1,2932 @@ +// +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("20241125185632_AddUseRiskInsightsFlag")] + partial class AddUseRiskInsightsFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionCreationDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseRiskInsights") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResponseDate") + .HasColumnType("TEXT"); + + b.Property("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GranteeId") + .HasColumnType("TEXT"); + + b.Property("GrantorId") + .HasColumnType("TEXT"); + + b.Property("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WaitTimeDays") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AaGuid") + .HasColumnType("TEXT"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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.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.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + 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/20241125185632_AddUseRiskInsightsFlag.cs b/util/SqliteMigrations/Migrations/20241125185632_AddUseRiskInsightsFlag.cs new file mode 100644 index 0000000000..86ff055fc5 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20241125185632_AddUseRiskInsightsFlag.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddUseRiskInsightsFlag : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UseRiskInsights", + table: "Organization", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UseRiskInsights", + table: "Organization"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 67390bcbcb..f906543254 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -186,6 +186,9 @@ namespace Bit.SqliteMigrations.Migrations b.Property("UseResetPassword") .HasColumnType("INTEGER"); + b.Property("UseRiskInsights") + .HasColumnType("INTEGER"); + b.Property("UseScim") .HasColumnType("INTEGER"); From 6a9b7ece2bbfbabbc9f33d7a440f6c892c30011d Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 6 Dec 2024 08:07:04 +1000 Subject: [PATCH 39/94] [PM-11360] Remove export permission for providers (#5051) - also fix managed collections export from CLI --- .../VaultExportAuthorizationHandler.cs | 38 ++++++++ .../Authorization/VaultExportOperations.cs | 20 ++++ .../OrganizationExportController.cs | 54 ++++++++++- .../OrganizationExportResponseModel.cs | 10 ++ .../Utilities/ServiceCollectionExtensions.cs | 4 +- .../Models/Response/CipherResponseModel.cs | 7 ++ src/Core/Constants.cs | 1 + .../Queries/IOrganizationCiphersQuery.cs | 10 ++ .../Vault/Queries/OrganizationCiphersQuery.cs | 9 ++ .../VaultExportAuthorizationHandlerTests.cs | 95 +++++++++++++++++++ .../Helpers/AuthorizationHelpers.cs | 52 ++++++++++ .../Helpers/AuthorizationHelpersTests.cs | 38 ++++++++ .../Queries/OrganizationCiphersQueryTests.cs | 92 ++++++++++++++++++ 13 files changed, 428 insertions(+), 2 deletions(-) create mode 100644 src/Api/Tools/Authorization/VaultExportAuthorizationHandler.cs create mode 100644 src/Api/Tools/Authorization/VaultExportOperations.cs create mode 100644 test/Api.Test/Tools/Authorization/VaultExportAuthorizationHandlerTests.cs create mode 100644 test/Core.Test/AdminConsole/Helpers/AuthorizationHelpers.cs create mode 100644 test/Core.Test/AdminConsole/Helpers/AuthorizationHelpersTests.cs create mode 100644 test/Core.Test/Vault/Queries/OrganizationCiphersQueryTests.cs diff --git a/src/Api/Tools/Authorization/VaultExportAuthorizationHandler.cs b/src/Api/Tools/Authorization/VaultExportAuthorizationHandler.cs new file mode 100644 index 0000000000..337a0dc1e5 --- /dev/null +++ b/src/Api/Tools/Authorization/VaultExportAuthorizationHandler.cs @@ -0,0 +1,38 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +using Bit.Core.Context; +using Bit.Core.Enums; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Api.Tools.Authorization; + +public class VaultExportAuthorizationHandler(ICurrentContext currentContext) + : AuthorizationHandler +{ + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, + VaultExportOperationRequirement requirement, OrganizationScope organizationScope) + { + var org = currentContext.GetOrganization(organizationScope); + + var authorized = requirement switch + { + not null when requirement == VaultExportOperations.ExportWholeVault => + CanExportWholeVault(org), + not null when requirement == VaultExportOperations.ExportManagedCollections => + CanExportManagedCollections(org), + _ => false + }; + + if (authorized) + { + context.Succeed(requirement); + } + + return Task.FromResult(0); + } + + private bool CanExportWholeVault(CurrentContextOrganization organization) => organization is + { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or + { Type: OrganizationUserType.Custom, Permissions.AccessImportExport: true }; + + private bool CanExportManagedCollections(CurrentContextOrganization organization) => organization is not null; +} diff --git a/src/Api/Tools/Authorization/VaultExportOperations.cs b/src/Api/Tools/Authorization/VaultExportOperations.cs new file mode 100644 index 0000000000..c88d2c80b1 --- /dev/null +++ b/src/Api/Tools/Authorization/VaultExportOperations.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Api.Tools.Authorization; + +public class VaultExportOperationRequirement : OperationAuthorizationRequirement; + +public static class VaultExportOperations +{ + /// + /// Exporting the entire organization vault. + /// + public static readonly VaultExportOperationRequirement ExportWholeVault = + new() { Name = nameof(ExportWholeVault) }; + + /// + /// Exporting only the organization items that the user has Can Manage permissions for + /// + public static readonly VaultExportOperationRequirement ExportManagedCollections = + new() { Name = nameof(ExportManagedCollections) }; +} diff --git a/src/Api/Tools/Controllers/OrganizationExportController.cs b/src/Api/Tools/Controllers/OrganizationExportController.cs index b3c0643b28..144e1be69e 100644 --- a/src/Api/Tools/Controllers/OrganizationExportController.cs +++ b/src/Api/Tools/Controllers/OrganizationExportController.cs @@ -1,11 +1,17 @@ using Bit.Api.Models.Response; +using Bit.Api.Tools.Authorization; using Bit.Api.Tools.Models.Response; using Bit.Api.Vault.Models.Response; +using Bit.Core; +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.Context; using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Queries; using Bit.Core.Vault.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -21,24 +27,41 @@ public class OrganizationExportController : Controller private readonly ICollectionService _collectionService; private readonly ICipherService _cipherService; private readonly GlobalSettings _globalSettings; + private readonly IFeatureService _featureService; + private readonly IAuthorizationService _authorizationService; + private readonly IOrganizationCiphersQuery _organizationCiphersQuery; + private readonly ICollectionRepository _collectionRepository; public OrganizationExportController( ICurrentContext currentContext, ICipherService cipherService, ICollectionService collectionService, IUserService userService, - GlobalSettings globalSettings) + GlobalSettings globalSettings, + IFeatureService featureService, + IAuthorizationService authorizationService, + IOrganizationCiphersQuery organizationCiphersQuery, + ICollectionRepository collectionRepository) { _currentContext = currentContext; _cipherService = cipherService; _collectionService = collectionService; _userService = userService; _globalSettings = globalSettings; + _featureService = featureService; + _authorizationService = authorizationService; + _organizationCiphersQuery = organizationCiphersQuery; + _collectionRepository = collectionRepository; } [HttpGet("export")] public async Task Export(Guid organizationId) { + if (_featureService.IsEnabled(FeatureFlagKeys.PM11360RemoveProviderExportPermission)) + { + return await Export_vNext(organizationId); + } + var userId = _userService.GetProperUserId(User).Value; IEnumerable orgCollections = await _collectionService.GetOrganizationCollectionsAsync(organizationId); @@ -65,6 +88,35 @@ public class OrganizationExportController : Controller return Ok(organizationExportListResponseModel); } + private async Task Export_vNext(Guid organizationId) + { + var canExportAll = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(organizationId), + VaultExportOperations.ExportWholeVault); + if (canExportAll.Succeeded) + { + var allOrganizationCiphers = await _organizationCiphersQuery.GetAllOrganizationCiphers(organizationId); + var allCollections = await _collectionRepository.GetManyByOrganizationIdAsync(organizationId); + return Ok(new OrganizationExportResponseModel(allOrganizationCiphers, allCollections, _globalSettings)); + } + + var canExportManaged = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(organizationId), + VaultExportOperations.ExportManagedCollections); + if (canExportManaged.Succeeded) + { + var userId = _userService.GetProperUserId(User)!.Value; + + var allUserCollections = await _collectionRepository.GetManyByUserIdAsync(userId); + var managedOrgCollections = allUserCollections.Where(c => c.OrganizationId == organizationId && c.Manage).ToList(); + var managedCiphers = + await _organizationCiphersQuery.GetOrganizationCiphersByCollectionIds(organizationId, managedOrgCollections.Select(c => c.Id)); + + return Ok(new OrganizationExportResponseModel(managedCiphers, managedOrgCollections, _globalSettings)); + } + + // Unauthorized + throw new NotFoundException(); + } + private ListResponseModel GetOrganizationCollectionsResponse(IEnumerable orgCollections) { var collections = orgCollections.Select(c => new CollectionResponseModel(c)); diff --git a/src/Api/Tools/Models/Response/OrganizationExportResponseModel.cs b/src/Api/Tools/Models/Response/OrganizationExportResponseModel.cs index a4b35d8de1..5fd7e821cf 100644 --- a/src/Api/Tools/Models/Response/OrganizationExportResponseModel.cs +++ b/src/Api/Tools/Models/Response/OrganizationExportResponseModel.cs @@ -1,6 +1,9 @@ using Bit.Api.Models.Response; using Bit.Api.Vault.Models.Response; +using Bit.Core.Entities; using Bit.Core.Models.Api; +using Bit.Core.Settings; +using Bit.Core.Vault.Models.Data; namespace Bit.Api.Tools.Models.Response; @@ -10,6 +13,13 @@ public class OrganizationExportResponseModel : ResponseModel { } + public OrganizationExportResponseModel(IEnumerable ciphers, + IEnumerable collections, GlobalSettings globalSettings) : this() + { + Ciphers = ciphers.Select(c => new CipherMiniDetailsResponseModel(c, globalSettings)); + Collections = collections.Select(c => new CollectionResponseModel(c)); + } + public IEnumerable Collections { get; set; } public IEnumerable Ciphers { get; set; } diff --git a/src/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index 8a58a5f236..3d206fd887 100644 --- a/src/Api/Utilities/ServiceCollectionExtensions.cs +++ b/src/Api/Utilities/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using Bit.Api.Vault.AuthorizationHandlers.Collections; +using Bit.Api.Tools.Authorization; +using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; using Bit.Core.IdentityServer; using Bit.Core.Settings; @@ -99,5 +100,6 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Api/Vault/Models/Response/CipherResponseModel.cs b/src/Api/Vault/Models/Response/CipherResponseModel.cs index 10b77274b5..207017227a 100644 --- a/src/Api/Vault/Models/Response/CipherResponseModel.cs +++ b/src/Api/Vault/Models/Response/CipherResponseModel.cs @@ -166,5 +166,12 @@ public class CipherMiniDetailsResponseModel : CipherMiniResponseModel CollectionIds = cipher.CollectionIds ?? new List(); } + public CipherMiniDetailsResponseModel(CipherOrganizationDetailsWithCollections cipher, + GlobalSettings globalSettings, string obj = "cipherMiniDetails") + : base(cipher, globalSettings, cipher.OrganizationUseTotp, obj) + { + CollectionIds = cipher.CollectionIds ?? new List(); + } + public IEnumerable CollectionIds { get; set; } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 14258353d6..8c1456793d 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -153,6 +153,7 @@ public static class FeatureFlagKeys public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss"; public const string SecurityTasks = "security-tasks"; public const string PM14401_ScaleMSPOnClientOrganizationUpdate = "PM-14401-scale-msp-on-client-organization-update"; + public const string PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission"; public const string DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship"; public const string MacOsNativeCredentialSync = "macos-native-credential-sync"; public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; diff --git a/src/Core/Vault/Queries/IOrganizationCiphersQuery.cs b/src/Core/Vault/Queries/IOrganizationCiphersQuery.cs index 680743088e..1756cad3c7 100644 --- a/src/Core/Vault/Queries/IOrganizationCiphersQuery.cs +++ b/src/Core/Vault/Queries/IOrganizationCiphersQuery.cs @@ -27,4 +27,14 @@ public interface IOrganizationCiphersQuery ///
/// Task> GetUnassignedOrganizationCiphers(Guid organizationId); + + /// + /// Returns ciphers belonging to the organization that are in the specified collections. + /// + /// + /// Note that the will include all collections + /// the cipher belongs to even if it is not in the parameter. + /// + public Task> GetOrganizationCiphersByCollectionIds( + Guid organizationId, IEnumerable collectionIds); } diff --git a/src/Core/Vault/Queries/OrganizationCiphersQuery.cs b/src/Core/Vault/Queries/OrganizationCiphersQuery.cs index f91e3cbbbb..deed121216 100644 --- a/src/Core/Vault/Queries/OrganizationCiphersQuery.cs +++ b/src/Core/Vault/Queries/OrganizationCiphersQuery.cs @@ -52,4 +52,13 @@ public class OrganizationCiphersQuery : IOrganizationCiphersQuery { return await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organizationId); } + + /// + public async Task> GetOrganizationCiphersByCollectionIds( + Guid organizationId, IEnumerable collectionIds) + { + var managedCollectionIds = collectionIds.ToHashSet(); + var allOrganizationCiphers = await GetAllOrganizationCiphers(organizationId); + return allOrganizationCiphers.Where(c => c.CollectionIds.Intersect(managedCollectionIds).Any()); + } } diff --git a/test/Api.Test/Tools/Authorization/VaultExportAuthorizationHandlerTests.cs b/test/Api.Test/Tools/Authorization/VaultExportAuthorizationHandlerTests.cs new file mode 100644 index 0000000000..6c42205b1a --- /dev/null +++ b/test/Api.Test/Tools/Authorization/VaultExportAuthorizationHandlerTests.cs @@ -0,0 +1,95 @@ +using System.Security.Claims; +using Bit.Api.Tools.Authorization; +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Test.AdminConsole.Helpers; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.Tools.Authorization; + +[SutProviderCustomize] +public class VaultExportAuthorizationHandlerTests +{ + public static IEnumerable CanExportWholeVault => new List + { + new () { Type = OrganizationUserType.Owner }, + new () { Type = OrganizationUserType.Admin }, + new () + { + Type = OrganizationUserType.Custom, Permissions = new Permissions { AccessImportExport = true } + } + }.Select(org => new[] { org }); + + [Theory] + [BitMemberAutoData(nameof(CanExportWholeVault))] + public async Task ExportAll_PermittedRoles_Success(CurrentContextOrganization org, OrganizationScope orgScope, ClaimsPrincipal user, + SutProvider sutProvider) + { + org.Id = orgScope; + sutProvider.GetDependency().GetOrganization(orgScope).Returns(org); + + var authContext = new AuthorizationHandlerContext(new[] { VaultExportOperations.ExportWholeVault }, user, orgScope); + await sutProvider.Sut.HandleAsync(authContext); + + Assert.True(authContext.HasSucceeded); + } + + public static IEnumerable CannotExportWholeVault => new List + { + new () { Type = OrganizationUserType.User }, + new () + { + Type = OrganizationUserType.Custom, Permissions = new Permissions { AccessImportExport = true }.Invert() + } + }.Select(org => new[] { org }); + + [Theory] + [BitMemberAutoData(nameof(CannotExportWholeVault))] + public async Task ExportAll_NotPermitted_Failure(CurrentContextOrganization org, OrganizationScope orgScope, ClaimsPrincipal user, + SutProvider sutProvider) + { + org.Id = orgScope; + sutProvider.GetDependency().GetOrganization(orgScope).Returns(org); + + var authContext = new AuthorizationHandlerContext(new[] { VaultExportOperations.ExportWholeVault }, user, orgScope); + await sutProvider.Sut.HandleAsync(authContext); + + Assert.False(authContext.HasSucceeded); + } + + public static IEnumerable CanExportManagedCollections => + AuthorizationHelpers.AllRoles().Select(o => new[] { o }); + + [Theory] + [BitMemberAutoData(nameof(CanExportManagedCollections))] + public async Task ExportManagedCollections_PermittedRoles_Success(CurrentContextOrganization org, OrganizationScope orgScope, ClaimsPrincipal user, + SutProvider sutProvider) + { + org.Id = orgScope; + sutProvider.GetDependency().GetOrganization(orgScope).Returns(org); + + var authContext = new AuthorizationHandlerContext(new[] { VaultExportOperations.ExportManagedCollections }, user, orgScope); + await sutProvider.Sut.HandleAsync(authContext); + + Assert.True(authContext.HasSucceeded); + } + + [Theory] + [BitAutoData([null])] + public async Task ExportManagedCollections_NotPermitted_Failure(CurrentContextOrganization org, OrganizationScope orgScope, ClaimsPrincipal user, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetOrganization(orgScope).Returns(org); + + var authContext = new AuthorizationHandlerContext(new[] { VaultExportOperations.ExportManagedCollections }, user, orgScope); + await sutProvider.Sut.HandleAsync(authContext); + + Assert.False(authContext.HasSucceeded); + } +} diff --git a/test/Core.Test/AdminConsole/Helpers/AuthorizationHelpers.cs b/test/Core.Test/AdminConsole/Helpers/AuthorizationHelpers.cs new file mode 100644 index 0000000000..854cdcb3c8 --- /dev/null +++ b/test/Core.Test/AdminConsole/Helpers/AuthorizationHelpers.cs @@ -0,0 +1,52 @@ +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Models.Data; + +namespace Bit.Core.Test.AdminConsole.Helpers; + +public static class AuthorizationHelpers +{ + /// + /// Return a new Permission object with inverted permissions. + /// This is useful to test negative cases, e.g. "all other permissions should fail". + /// + /// + /// + public static Permissions Invert(this Permissions permissions) + { + // Get all false boolean properties of input object + var inputsToFlip = permissions + .GetType() + .GetProperties() + .Where(p => + p.PropertyType == typeof(bool) && + (bool)p.GetValue(permissions, null)! == false) + .Select(p => p.Name); + + var result = new Permissions(); + + // Set these to true on the result object + result + .GetType() + .GetProperties() + .Where(p => inputsToFlip.Contains(p.Name)) + .ToList() + .ForEach(p => p.SetValue(result, true)); + + return result; + } + + /// + /// Returns a sequence of all possible roles and permissions represented as CurrentContextOrganization objects. + /// Used largely for authorization testing. + /// + /// + public static IEnumerable AllRoles() => new List + { + new () { Type = OrganizationUserType.Owner }, + new () { Type = OrganizationUserType.Admin }, + new () { Type = OrganizationUserType.Custom, Permissions = new Permissions() }, + new () { Type = OrganizationUserType.Custom, Permissions = new Permissions().Invert() }, + new () { Type = OrganizationUserType.User }, + }; +} diff --git a/test/Core.Test/AdminConsole/Helpers/AuthorizationHelpersTests.cs b/test/Core.Test/AdminConsole/Helpers/AuthorizationHelpersTests.cs new file mode 100644 index 0000000000..db128ffc4b --- /dev/null +++ b/test/Core.Test/AdminConsole/Helpers/AuthorizationHelpersTests.cs @@ -0,0 +1,38 @@ +using Bit.Core.Models.Data; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.Helpers; + +public class AuthorizationHelpersTests +{ + [Fact] + public void Permissions_Invert_InvertsAllPermissions() + { + var sut = new Permissions + { + AccessEventLogs = true, + AccessReports = true, + DeleteAnyCollection = true, + ManagePolicies = true, + ManageScim = true + }; + + var result = sut.Invert(); + + Assert.True(result is + { + AccessEventLogs: false, + AccessImportExport: true, + AccessReports: false, + CreateNewCollections: true, + EditAnyCollection: true, + DeleteAnyCollection: false, + ManageGroups: true, + ManagePolicies: false, + ManageSso: true, + ManageUsers: true, + ManageResetPassword: true, + ManageScim: false + }); + } +} diff --git a/test/Core.Test/Vault/Queries/OrganizationCiphersQueryTests.cs b/test/Core.Test/Vault/Queries/OrganizationCiphersQueryTests.cs new file mode 100644 index 0000000000..01539fe7d7 --- /dev/null +++ b/test/Core.Test/Vault/Queries/OrganizationCiphersQueryTests.cs @@ -0,0 +1,92 @@ +using AutoFixture; +using Bit.Core.Entities; +using Bit.Core.Repositories; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Queries; +using Bit.Core.Vault.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Vault.Queries; + +[SutProviderCustomize] +public class OrganizationCiphersQueryTests +{ + [Theory, BitAutoData] + public async Task GetOrganizationCiphersInCollections_ReturnsFilteredCiphers( + Guid organizationId, SutProvider sutProvider) + { + var fixture = new Fixture(); + + var otherCollectionId = Guid.NewGuid(); + var targetCollectionId = Guid.NewGuid(); + + var otherCipher = fixture.Create(); + var targetCipher = fixture.Create(); + var bothCipher = fixture.Create(); + var noCipher = fixture.Create(); + + var ciphers = new List + { + otherCipher, // not in the target collection + targetCipher, // in the target collection + bothCipher, // in both collections + noCipher // not in any collection + }; + ciphers.ForEach(c => + { + c.OrganizationId = organizationId; + c.UserId = null; + }); + + var otherCollectionCipher = new CollectionCipher + { + CollectionId = otherCollectionId, + CipherId = otherCipher.Id + }; + var targetCollectionCipher = new CollectionCipher + { + CollectionId = targetCollectionId, + CipherId = targetCipher.Id + }; + var bothCollectionCipher1 = new CollectionCipher + { + CollectionId = targetCollectionId, + CipherId = bothCipher.Id + }; + var bothCollectionCipher2 = new CollectionCipher + { + CollectionId = otherCollectionId, + CipherId = bothCipher.Id + }; + + sutProvider.GetDependency().GetManyOrganizationDetailsByOrganizationIdAsync(organizationId) + .Returns(ciphers); + + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organizationId).Returns( + [ + targetCollectionCipher, + otherCollectionCipher, + bothCollectionCipher1, + bothCollectionCipher2 + ]); + + var result = await sutProvider + .Sut + .GetOrganizationCiphersByCollectionIds(organizationId, [targetCollectionId]); + result = result.ToList(); + + Assert.Equal(2, result.Count()); + Assert.Contains(result, c => + c.Id == targetCipher.Id && + c.CollectionIds.Count() == 1 && + c.CollectionIds.Any(cId => cId == targetCollectionId)); + Assert.Contains(result, c => + c.Id == bothCipher.Id && + c.CollectionIds.Count() == 2 && + c.CollectionIds.Any(cId => cId == targetCollectionId) && + c.CollectionIds.Any(cId => cId == otherCollectionId)); + } +} From 2333a934a97320bc189b6e37c47067953f398c40 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Thu, 5 Dec 2024 21:18:02 -0600 Subject: [PATCH 40/94] [PM-12488] Migrating Cloud Org Sign Up to Command (#5078) --- .../Controllers/OrganizationsController.cs | 17 +- .../CloudOrganizationSignUpCommand.cs | 368 ++++++++++++++++++ .../Services/IOrganizationService.cs | 6 - .../Implementations/OrganizationService.cs | 124 ------ ...OrganizationServiceCollectionExtensions.cs | 5 + .../Helpers/OrganizationTestHelpers.cs | 10 +- .../OrganizationsControllerTests.cs | 9 +- .../CloudOrganizationSignUpCommandTests.cs | 238 +++++++++++ .../Services/OrganizationServiceTests.cs | 216 ---------- 9 files changed, 630 insertions(+), 363 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 0ac750e665..6ffd60e425 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; @@ -52,11 +53,11 @@ public class OrganizationsController : Controller private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly IFeatureService _featureService; private readonly GlobalSettings _globalSettings; - private readonly IPushNotificationService _pushNotificationService; private readonly IProviderRepository _providerRepository; private readonly IProviderBillingService _providerBillingService; private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; + private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand; public OrganizationsController( IOrganizationRepository organizationRepository, @@ -73,11 +74,11 @@ public class OrganizationsController : Controller IOrganizationApiKeyRepository organizationApiKeyRepository, IFeatureService featureService, GlobalSettings globalSettings, - IPushNotificationService pushNotificationService, IProviderRepository providerRepository, IProviderBillingService providerBillingService, IDataProtectorTokenFactory orgDeleteTokenDataFactory, - IRemoveOrganizationUserCommand removeOrganizationUserCommand) + IRemoveOrganizationUserCommand removeOrganizationUserCommand, + ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -93,11 +94,11 @@ public class OrganizationsController : Controller _organizationApiKeyRepository = organizationApiKeyRepository; _featureService = featureService; _globalSettings = globalSettings; - _pushNotificationService = pushNotificationService; _providerRepository = providerRepository; _providerBillingService = providerBillingService; _orgDeleteTokenDataFactory = orgDeleteTokenDataFactory; _removeOrganizationUserCommand = removeOrganizationUserCommand; + _cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand; } [HttpGet("{id}")] @@ -175,8 +176,8 @@ public class OrganizationsController : Controller } var organizationSignup = model.ToOrganizationSignup(user); - var result = await _organizationService.SignUpAsync(organizationSignup); - return new OrganizationResponseModel(result.Item1); + var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup); + return new OrganizationResponseModel(result.Organization); } [HttpPost("create-without-payment")] @@ -190,8 +191,8 @@ public class OrganizationsController : Controller } var organizationSignup = model.ToOrganizationSignup(user); - var result = await _organizationService.SignUpAsync(organizationSignup); - return new OrganizationResponseModel(result.Item1); + var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup); + return new OrganizationResponseModel(result.Organization); } [HttpPut("{id}")] diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs new file mode 100644 index 0000000000..3eb4d35ef1 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs @@ -0,0 +1,368 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Services; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Services; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.Data; +using Bit.Core.Models.StaticStore; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Tools.Services; +using Bit.Core.Utilities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; + +public record SignUpOrganizationResponse( + Organization Organization, + OrganizationUser OrganizationUser, + Collection DefaultCollection); + +public interface ICloudOrganizationSignUpCommand +{ + Task SignUpOrganizationAsync(OrganizationSignup signup); +} + +public class CloudOrganizationSignUpCommand( + IOrganizationUserRepository organizationUserRepository, + IFeatureService featureService, + IOrganizationBillingService organizationBillingService, + IPaymentService paymentService, + IPolicyService policyService, + IReferenceEventService referenceEventService, + ICurrentContext currentContext, + IOrganizationRepository organizationRepository, + IOrganizationApiKeyRepository organizationApiKeyRepository, + IApplicationCacheService applicationCacheService, + IPushRegistrationService pushRegistrationService, + IPushNotificationService pushNotificationService, + ICollectionRepository collectionRepository, + IDeviceRepository deviceRepository) : ICloudOrganizationSignUpCommand +{ + public async Task SignUpOrganizationAsync(OrganizationSignup signup) + { + var plan = StaticStore.GetPlan(signup.Plan); + + ValidatePasswordManagerPlan(plan, signup); + + if (signup.UseSecretsManager) + { + if (signup.IsFromProvider) + { + throw new BadRequestException( + "Organizations with a Managed Service Provider do not support Secrets Manager."); + } + ValidateSecretsManagerPlan(plan, signup); + } + + if (!signup.IsFromProvider) + { + await ValidateSignUpPoliciesAsync(signup.Owner.Id); + } + + var organization = new Organization + { + // Pre-generate the org id so that we can save it with the Stripe subscription + Id = CoreHelpers.GenerateComb(), + Name = signup.Name, + BillingEmail = signup.BillingEmail, + BusinessName = signup.BusinessName, + PlanType = plan!.Type, + Seats = (short)(plan.PasswordManager.BaseSeats + signup.AdditionalSeats), + MaxCollections = plan.PasswordManager.MaxCollections, + MaxStorageGb = !plan.PasswordManager.BaseStorageGb.HasValue ? + (short?)null : (short)(plan.PasswordManager.BaseStorageGb.Value + signup.AdditionalStorageGb), + UsePolicies = plan.HasPolicies, + UseSso = plan.HasSso, + UseGroups = plan.HasGroups, + UseEvents = plan.HasEvents, + UseDirectory = plan.HasDirectory, + UseTotp = plan.HasTotp, + Use2fa = plan.Has2fa, + UseApi = plan.HasApi, + UseResetPassword = plan.HasResetPassword, + SelfHost = plan.HasSelfHost, + UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon, + UseCustomPermissions = plan.HasCustomPermissions, + UseScim = plan.HasScim, + Plan = plan.Name, + Gateway = null, + ReferenceData = signup.Owner.ReferenceData, + Enabled = true, + LicenseKey = CoreHelpers.SecureRandomString(20), + PublicKey = signup.PublicKey, + PrivateKey = signup.PrivateKey, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Status = OrganizationStatusType.Created, + UsePasswordManager = true, + UseSecretsManager = signup.UseSecretsManager + }; + + if (signup.UseSecretsManager) + { + organization.SmSeats = plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats.GetValueOrDefault(); + organization.SmServiceAccounts = plan.SecretsManager.BaseServiceAccount + + signup.AdditionalServiceAccounts.GetValueOrDefault(); + } + + if (plan.Type == PlanType.Free && !signup.IsFromProvider) + { + var adminCount = + await organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id); + if (adminCount > 0) + { + throw new BadRequestException("You can only be an admin of one free organization."); + } + } + else if (plan.Type != PlanType.Free) + { + if (featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI)) + { + var sale = OrganizationSale.From(organization, signup); + await organizationBillingService.Finalize(sale); + } + else + { + if (signup.PaymentMethodType != null) + { + await paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value, + signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats, + signup.PremiumAccessAddon, signup.TaxInfo, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(), + signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial); + } + else + { + await paymentService.PurchaseOrganizationNoPaymentMethod(organization, plan, signup.AdditionalSeats, + signup.PremiumAccessAddon, signup.AdditionalSmSeats.GetValueOrDefault(), + signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial); + } + + } + } + + var ownerId = signup.IsFromProvider ? default : signup.Owner.Id; + var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true); + await referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.Signup, organization, currentContext) + { + PlanName = plan.Name, + PlanType = plan.Type, + Seats = returnValue.Item1.Seats, + SignupInitiationPath = signup.InitiationPath, + Storage = returnValue.Item1.MaxStorageGb, + // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 + }); + + return new SignUpOrganizationResponse(returnValue.organization, returnValue.organizationUser, returnValue.defaultCollection); + } + + public void ValidatePasswordManagerPlan(Plan plan, OrganizationUpgrade upgrade) + { + ValidatePlan(plan, upgrade.AdditionalSeats, "Password Manager"); + + if (plan.PasswordManager.BaseSeats + upgrade.AdditionalSeats <= 0) + { + throw new BadRequestException($"You do not have any Password Manager seats!"); + } + + if (upgrade.AdditionalSeats < 0) + { + throw new BadRequestException($"You can't subtract Password Manager seats!"); + } + + if (!plan.PasswordManager.HasAdditionalStorageOption && upgrade.AdditionalStorageGb > 0) + { + throw new BadRequestException("Plan does not allow additional storage."); + } + + if (upgrade.AdditionalStorageGb < 0) + { + throw new BadRequestException("You can't subtract storage!"); + } + + if (!plan.PasswordManager.HasPremiumAccessOption && upgrade.PremiumAccessAddon) + { + throw new BadRequestException("This plan does not allow you to buy the premium access addon."); + } + + if (!plan.PasswordManager.HasAdditionalSeatsOption && upgrade.AdditionalSeats > 0) + { + throw new BadRequestException("Plan does not allow additional users."); + } + + if (plan.PasswordManager.HasAdditionalSeatsOption && plan.PasswordManager.MaxAdditionalSeats.HasValue && + upgrade.AdditionalSeats > plan.PasswordManager.MaxAdditionalSeats.Value) + { + throw new BadRequestException($"Selected plan allows a maximum of " + + $"{plan.PasswordManager.MaxAdditionalSeats.GetValueOrDefault(0)} additional users."); + } + } + + public void ValidateSecretsManagerPlan(Plan plan, OrganizationUpgrade upgrade) + { + if (plan.SupportsSecretsManager == false) + { + throw new BadRequestException("Invalid Secrets Manager plan selected."); + } + + ValidatePlan(plan, upgrade.AdditionalSmSeats.GetValueOrDefault(), "Secrets Manager"); + + if (plan.SecretsManager.BaseSeats + upgrade.AdditionalSmSeats <= 0) + { + throw new BadRequestException($"You do not have any Secrets Manager seats!"); + } + + if (!plan.SecretsManager.HasAdditionalServiceAccountOption && upgrade.AdditionalServiceAccounts > 0) + { + throw new BadRequestException("Plan does not allow additional Machine Accounts."); + } + + if ((plan.ProductTier == ProductTierType.TeamsStarter && + upgrade.AdditionalSmSeats.GetValueOrDefault() > plan.PasswordManager.BaseSeats) || + (plan.ProductTier != ProductTierType.TeamsStarter && + upgrade.AdditionalSmSeats.GetValueOrDefault() > upgrade.AdditionalSeats)) + { + throw new BadRequestException("You cannot have more Secrets Manager seats than Password Manager seats."); + } + + if (upgrade.AdditionalServiceAccounts.GetValueOrDefault() < 0) + { + throw new BadRequestException("You can't subtract Machine Accounts!"); + } + + switch (plan.SecretsManager.HasAdditionalSeatsOption) + { + case false when upgrade.AdditionalSmSeats > 0: + throw new BadRequestException("Plan does not allow additional users."); + case true when plan.SecretsManager.MaxAdditionalSeats.HasValue && + upgrade.AdditionalSmSeats > plan.SecretsManager.MaxAdditionalSeats.Value: + throw new BadRequestException($"Selected plan allows a maximum of " + + $"{plan.SecretsManager.MaxAdditionalSeats.GetValueOrDefault(0)} additional users."); + } + } + + private static void ValidatePlan(Plan plan, int additionalSeats, string productType) + { + if (plan is null) + { + throw new BadRequestException($"{productType} Plan was null."); + } + + if (plan.Disabled) + { + throw new BadRequestException($"{productType} Plan not found."); + } + + if (additionalSeats < 0) + { + throw new BadRequestException($"You can't subtract {productType} seats!"); + } + } + + private async Task ValidateSignUpPoliciesAsync(Guid ownerId) + { + var anySingleOrgPolicies = await policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg); + if (anySingleOrgPolicies) + { + throw new BadRequestException("You may not create an organization. You belong to an organization " + + "which has a policy that prohibits you from being a member of any other organization."); + } + } + + private async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(Organization organization, + Guid ownerId, string ownerKey, string collectionName, bool withPayment) + { + try + { + await organizationRepository.CreateAsync(organization); + await organizationApiKeyRepository.CreateAsync(new OrganizationApiKey + { + OrganizationId = organization.Id, + ApiKey = CoreHelpers.SecureRandomString(30), + Type = OrganizationApiKeyType.Default, + RevisionDate = DateTime.UtcNow, + }); + await applicationCacheService.UpsertOrganizationAbilityAsync(organization); + + // ownerId == default if the org is created by a provider - in this case it's created without an + // owner and the first owner is immediately invited afterwards + OrganizationUser orgUser = null; + if (ownerId != default) + { + orgUser = new OrganizationUser + { + OrganizationId = organization.Id, + UserId = ownerId, + Key = ownerKey, + AccessSecretsManager = organization.UseSecretsManager, + Type = OrganizationUserType.Owner, + Status = OrganizationUserStatusType.Confirmed, + CreationDate = organization.CreationDate, + RevisionDate = organization.CreationDate + }; + orgUser.SetNewId(); + + await organizationUserRepository.CreateAsync(orgUser); + + var devices = await GetUserDeviceIdsAsync(orgUser.UserId.Value); + await pushRegistrationService.AddUserRegistrationOrganizationAsync(devices, organization.Id.ToString()); + await pushNotificationService.PushSyncOrgKeysAsync(ownerId); + } + + Collection defaultCollection = null; + if (!string.IsNullOrWhiteSpace(collectionName)) + { + defaultCollection = new Collection + { + Name = collectionName, + OrganizationId = organization.Id, + CreationDate = organization.CreationDate, + RevisionDate = organization.CreationDate + }; + + // Give the owner Can Manage access over the default collection + List defaultOwnerAccess = null; + if (orgUser != null) + { + defaultOwnerAccess = + [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }]; + } + + await collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess); + } + + return (organization, orgUser, defaultCollection); + } + catch + { + if (withPayment) + { + await paymentService.CancelAndRecoverChargesAsync(organization); + } + + if (organization.Id != default(Guid)) + { + await organizationRepository.DeleteAsync(organization); + await applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id); + } + + throw; + } + } + + private async Task> GetUserDeviceIdsAsync(Guid userId) + { + var devices = await deviceRepository.GetManyByUserIdAsync(userId); + return devices + .Where(d => !string.IsNullOrWhiteSpace(d.PushToken)) + .Select(d => d.Id.ToString()); + } +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 646ae66166..0495c4c76e 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -20,13 +20,7 @@ public interface IOrganizationService Task AutoAddSeatsAsync(Organization organization, int seatsToAdd); Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment); Task VerifyBankAsync(Guid organizationId, int amount1, int amount2); - /// - /// Create a new organization in a cloud environment - /// - /// A tuple containing the new organization, the initial organizationUser (if any) and the default collection (if any) #nullable enable - Task<(Organization organization, OrganizationUser? organizationUser, Collection? defaultCollection)> SignUpAsync(OrganizationSignup organizationSignup); - Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup); #nullable disable /// diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 862b566c91..1ca047aa47 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -16,7 +16,6 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; -using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; @@ -502,129 +501,6 @@ public class OrganizationService : IOrganizationService return returnValue; } - /// - /// Create a new organization in a cloud environment - /// - public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(OrganizationSignup signup) - { - var plan = StaticStore.GetPlan(signup.Plan); - - ValidatePasswordManagerPlan(plan, signup); - - if (signup.UseSecretsManager) - { - if (signup.IsFromProvider) - { - throw new BadRequestException( - "Organizations with a Managed Service Provider do not support Secrets Manager."); - } - ValidateSecretsManagerPlan(plan, signup); - } - - if (!signup.IsFromProvider) - { - await ValidateSignUpPoliciesAsync(signup.Owner.Id); - } - - var organization = new Organization - { - // Pre-generate the org id so that we can save it with the Stripe subscription.. - Id = CoreHelpers.GenerateComb(), - Name = signup.Name, - BillingEmail = signup.BillingEmail, - BusinessName = signup.BusinessName, - PlanType = plan!.Type, - Seats = (short)(plan.PasswordManager.BaseSeats + signup.AdditionalSeats), - MaxCollections = plan.PasswordManager.MaxCollections, - MaxStorageGb = !plan.PasswordManager.BaseStorageGb.HasValue ? - (short?)null : (short)(plan.PasswordManager.BaseStorageGb.Value + signup.AdditionalStorageGb), - UsePolicies = plan.HasPolicies, - UseSso = plan.HasSso, - UseGroups = plan.HasGroups, - UseEvents = plan.HasEvents, - UseDirectory = plan.HasDirectory, - UseTotp = plan.HasTotp, - Use2fa = plan.Has2fa, - UseApi = plan.HasApi, - UseResetPassword = plan.HasResetPassword, - SelfHost = plan.HasSelfHost, - UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon, - UseCustomPermissions = plan.HasCustomPermissions, - UseScim = plan.HasScim, - Plan = plan.Name, - Gateway = null, - ReferenceData = signup.Owner.ReferenceData, - Enabled = true, - LicenseKey = CoreHelpers.SecureRandomString(20), - PublicKey = signup.PublicKey, - PrivateKey = signup.PrivateKey, - CreationDate = DateTime.UtcNow, - RevisionDate = DateTime.UtcNow, - Status = OrganizationStatusType.Created, - UsePasswordManager = true, - UseSecretsManager = signup.UseSecretsManager - }; - - if (signup.UseSecretsManager) - { - organization.SmSeats = plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats.GetValueOrDefault(); - organization.SmServiceAccounts = plan.SecretsManager.BaseServiceAccount + - signup.AdditionalServiceAccounts.GetValueOrDefault(); - } - - if (plan.Type == PlanType.Free && !signup.IsFromProvider) - { - var adminCount = - await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id); - if (adminCount > 0) - { - throw new BadRequestException("You can only be an admin of one free organization."); - } - } - else if (plan.Type != PlanType.Free) - { - var deprecateStripeSourcesAPI = _featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI); - - if (deprecateStripeSourcesAPI) - { - var sale = OrganizationSale.From(organization, signup); - await _organizationBillingService.Finalize(sale); - } - else - { - if (signup.PaymentMethodType != null) - { - await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value, - signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats, - signup.PremiumAccessAddon, signup.TaxInfo, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(), - signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial); - } - else - { - await _paymentService.PurchaseOrganizationNoPaymentMethod(organization, plan, signup.AdditionalSeats, - signup.PremiumAccessAddon, signup.AdditionalSmSeats.GetValueOrDefault(), - signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial); - } - - } - } - - var ownerId = signup.IsFromProvider ? default : signup.Owner.Id; - var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext) - { - PlanName = plan.Name, - PlanType = plan.Type, - Seats = returnValue.Item1.Seats, - SignupInitiationPath = signup.InitiationPath, - Storage = returnValue.Item1.MaxStorageGb, - // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 - }); - - return returnValue; - } - private async Task ValidateSignUpPoliciesAsync(Guid ownerId) { var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg); diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 96fdefcfad..5586273520 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; @@ -50,12 +51,16 @@ public static class OrganizationServiceCollectionExtensions services.AddOrganizationGroupCommands(); services.AddOrganizationLicenseCommandsQueries(); services.AddOrganizationDomainCommandsQueries(); + services.AddOrganizationSignUpCommands(); services.AddOrganizationAuthCommands(); services.AddOrganizationUserCommands(); services.AddOrganizationUserCommandsQueries(); services.AddBaseOrganizationSubscriptionCommandsQueries(); } + private static IServiceCollection AddOrganizationSignUpCommands(this IServiceCollection services) => + services.AddScoped(); + private static void AddOrganizationConnectionCommands(this IServiceCollection services) { services.AddScoped(); diff --git a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs index 64f719e82e..dd514803fe 100644 --- a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs +++ b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs @@ -1,13 +1,13 @@ using System.Diagnostics; using Bit.Api.IntegrationTest.Factories; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Models.Data; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.IntegrationTestCommon.Factories; namespace Bit.Api.IntegrationTest.Helpers; @@ -24,11 +24,11 @@ public static class OrganizationTestHelpers PaymentMethodType paymentMethod = PaymentMethodType.None) where T : class { var userRepository = factory.GetService(); - var organizationService = factory.GetService(); + var organizationSignUpCommand = factory.GetService(); var owner = await userRepository.GetByEmailAsync(ownerEmail); - var signUpResult = await organizationService.SignUpAsync(new OrganizationSignup + var signUpResult = await organizationSignUpCommand.SignUpOrganizationAsync(new OrganizationSignup { Name = name, BillingEmail = billingEmail, @@ -39,9 +39,9 @@ public static class OrganizationTestHelpers PaymentMethodType = paymentMethod }); - Debug.Assert(signUpResult.organizationUser is not null); + Debug.Assert(signUpResult.OrganizationUser is not null); - return new Tuple(signUpResult.organization, signUpResult.organizationUser); + return new Tuple(signUpResult.Organization, signUpResult.OrganizationUser); } /// diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 27c0f7a7c3..e58cd05b9d 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Entities; 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.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; @@ -46,11 +47,11 @@ public class OrganizationsControllerTests : IDisposable private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand; private readonly IFeatureService _featureService; - private readonly IPushNotificationService _pushNotificationService; private readonly IProviderRepository _providerRepository; private readonly IProviderBillingService _providerBillingService; private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; + private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand; private readonly OrganizationsController _sut; public OrganizationsControllerTests() @@ -69,11 +70,11 @@ public class OrganizationsControllerTests : IDisposable _userService = Substitute.For(); _createOrganizationApiKeyCommand = Substitute.For(); _featureService = Substitute.For(); - _pushNotificationService = Substitute.For(); _providerRepository = Substitute.For(); _providerBillingService = Substitute.For(); _orgDeleteTokenDataFactory = Substitute.For>(); _removeOrganizationUserCommand = Substitute.For(); + _cloudOrganizationSignUpCommand = Substitute.For(); _sut = new OrganizationsController( _organizationRepository, @@ -90,11 +91,11 @@ public class OrganizationsControllerTests : IDisposable _organizationApiKeyRepository, _featureService, _globalSettings, - _pushNotificationService, _providerRepository, _providerBillingService, _orgDeleteTokenDataFactory, - _removeOrganizationUserCommand); + _removeOrganizationUserCommand, + _cloudOrganizationSignUpCommand); } public void Dispose() diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs new file mode 100644 index 0000000000..2c32f0504b --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs @@ -0,0 +1,238 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.Billing.Enums; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.Data; +using Bit.Core.Models.StaticStore; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Tools.Services; +using Bit.Core.Utilities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations.OrganizationSignUp; + +[SutProviderCustomize] +public class CloudICloudOrganizationSignUpCommandTests +{ + [Theory] + [BitAutoData(PlanType.FamiliesAnnually)] + public async Task SignUp_PM_Family_Passes(PlanType planType, OrganizationSignup signup, SutProvider sutProvider) + { + signup.Plan = planType; + + var plan = StaticStore.GetPlan(signup.Plan); + + signup.AdditionalSeats = 0; + signup.PaymentMethodType = PaymentMethodType.Card; + signup.PremiumAccessAddon = false; + signup.UseSecretsManager = false; + signup.IsFromSecretsManagerTrial = false; + + var result = await sutProvider.Sut.SignUpOrganizationAsync(signup); + + await sutProvider.GetDependency().Received(1).CreateAsync( + Arg.Is(o => + o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats + && o.SmSeats == null + && o.SmServiceAccounts == null)); + await sutProvider.GetDependency().Received(1).CreateAsync( + Arg.Is(o => o.AccessSecretsManager == signup.UseSecretsManager)); + + await sutProvider.GetDependency().Received(1) + .RaiseEventAsync(Arg.Is(referenceEvent => + referenceEvent.Type == ReferenceEventType.Signup && + referenceEvent.PlanName == plan.Name && + referenceEvent.PlanType == plan.Type && + referenceEvent.Seats == result.Organization.Seats && + referenceEvent.Storage == result.Organization.MaxStorageGb)); + // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 + + Assert.NotNull(result.Organization); + Assert.NotNull(result.OrganizationUser); + + await sutProvider.GetDependency().Received(1).PurchaseOrganizationAsync( + Arg.Any(), + signup.PaymentMethodType.Value, + signup.PaymentToken, + plan, + signup.AdditionalStorageGb, + signup.AdditionalSeats, + signup.PremiumAccessAddon, + signup.TaxInfo, + false, + signup.AdditionalSmSeats.GetValueOrDefault(), + signup.AdditionalServiceAccounts.GetValueOrDefault(), + signup.UseSecretsManager + ); + } + + [Theory] + [BitAutoData(PlanType.FamiliesAnnually)] + public async Task SignUp_AssignsOwnerToDefaultCollection + (PlanType planType, OrganizationSignup signup, SutProvider sutProvider) + { + signup.Plan = planType; + signup.AdditionalSeats = 0; + signup.PaymentMethodType = PaymentMethodType.Card; + signup.PremiumAccessAddon = false; + signup.UseSecretsManager = false; + + // Extract orgUserId when created + Guid? orgUserId = null; + await sutProvider.GetDependency() + .CreateAsync(Arg.Do(ou => orgUserId = ou.Id)); + + var result = await sutProvider.Sut.SignUpOrganizationAsync(signup); + + // Assert: created a Can Manage association for the default collection + Assert.NotNull(orgUserId); + await sutProvider.GetDependency().Received(1).CreateAsync( + Arg.Any(), + Arg.Is>(cas => cas == null), + Arg.Is>(cas => + cas.Count() == 1 && + cas.All(c => + c.Id == orgUserId && + !c.ReadOnly && + !c.HidePasswords && + c.Manage))); + + Assert.NotNull(result.Organization); + Assert.NotNull(result.OrganizationUser); + } + + [Theory] + [BitAutoData(PlanType.EnterpriseAnnually)] + [BitAutoData(PlanType.EnterpriseMonthly)] + [BitAutoData(PlanType.TeamsAnnually)] + [BitAutoData(PlanType.TeamsMonthly)] + public async Task SignUp_SM_Passes(PlanType planType, OrganizationSignup signup, SutProvider sutProvider) + { + signup.Plan = planType; + + var plan = StaticStore.GetPlan(signup.Plan); + + signup.UseSecretsManager = true; + signup.AdditionalSeats = 15; + signup.AdditionalSmSeats = 10; + signup.AdditionalServiceAccounts = 20; + signup.PaymentMethodType = PaymentMethodType.Card; + signup.PremiumAccessAddon = false; + signup.IsFromSecretsManagerTrial = false; + + var result = await sutProvider.Sut.SignUpOrganizationAsync(signup); + + await sutProvider.GetDependency().Received(1).CreateAsync( + Arg.Is(o => + o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats + && o.SmSeats == plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats + && o.SmServiceAccounts == plan.SecretsManager.BaseServiceAccount + signup.AdditionalServiceAccounts)); + await sutProvider.GetDependency().Received(1).CreateAsync( + Arg.Is(o => o.AccessSecretsManager == signup.UseSecretsManager)); + + await sutProvider.GetDependency().Received(1) + .RaiseEventAsync(Arg.Is(referenceEvent => + referenceEvent.Type == ReferenceEventType.Signup && + referenceEvent.PlanName == plan.Name && + referenceEvent.PlanType == plan.Type && + referenceEvent.Seats == result.Organization.Seats && + referenceEvent.Storage == result.Organization.MaxStorageGb)); + // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 + + Assert.NotNull(result.Organization); + Assert.NotNull(result.OrganizationUser); + + await sutProvider.GetDependency().Received(1).PurchaseOrganizationAsync( + Arg.Any(), + signup.PaymentMethodType.Value, + signup.PaymentToken, + Arg.Is(plan), + signup.AdditionalStorageGb, + signup.AdditionalSeats, + signup.PremiumAccessAddon, + signup.TaxInfo, + false, + signup.AdditionalSmSeats.GetValueOrDefault(), + signup.AdditionalServiceAccounts.GetValueOrDefault(), + signup.IsFromSecretsManagerTrial + ); + } + + [Theory] + [BitAutoData(PlanType.EnterpriseAnnually)] + public async Task SignUp_SM_Throws_WhenManagedByMSP(PlanType planType, OrganizationSignup signup, SutProvider sutProvider) + { + signup.Plan = planType; + signup.UseSecretsManager = true; + signup.AdditionalSeats = 15; + signup.AdditionalSmSeats = 10; + signup.AdditionalServiceAccounts = 20; + signup.PaymentMethodType = PaymentMethodType.Card; + signup.PremiumAccessAddon = false; + signup.IsFromProvider = true; + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SignUpOrganizationAsync(signup)); + Assert.Contains("Organizations with a Managed Service Provider do not support Secrets Manager.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task SignUpAsync_SecretManager_AdditionalServiceAccounts_NotAllowedByPlan_ShouldThrowException(OrganizationSignup signup, SutProvider sutProvider) + { + signup.AdditionalSmSeats = 0; + signup.AdditionalSeats = 0; + signup.Plan = PlanType.Free; + signup.UseSecretsManager = true; + signup.PaymentMethodType = PaymentMethodType.Card; + signup.PremiumAccessAddon = false; + signup.AdditionalServiceAccounts = 10; + signup.AdditionalStorageGb = 0; + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpOrganizationAsync(signup)); + Assert.Contains("Plan does not allow additional Machine Accounts.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task SignUpAsync_SMSeatsGreatThanPMSeat_ShouldThrowException(OrganizationSignup signup, SutProvider sutProvider) + { + signup.AdditionalSmSeats = 100; + signup.AdditionalSeats = 10; + signup.Plan = PlanType.EnterpriseAnnually; + signup.UseSecretsManager = true; + signup.PaymentMethodType = PaymentMethodType.Card; + signup.PremiumAccessAddon = false; + signup.AdditionalServiceAccounts = 10; + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpOrganizationAsync(signup)); + Assert.Contains("You cannot have more Secrets Manager seats than Password Manager seats", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task SignUpAsync_InvalidateServiceAccount_ShouldThrowException(OrganizationSignup signup, SutProvider sutProvider) + { + signup.AdditionalSmSeats = 10; + signup.AdditionalSeats = 10; + signup.Plan = PlanType.EnterpriseAnnually; + signup.UseSecretsManager = true; + signup.PaymentMethodType = PaymentMethodType.Card; + signup.PremiumAccessAddon = false; + signup.AdditionalServiceAccounts = -10; + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpOrganizationAsync(signup)); + Assert.Contains("You can't subtract Machine Accounts!", exception.Message); + } +} diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index f0b7084fe9..fc839030aa 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -20,7 +20,6 @@ using Bit.Core.Models.Business; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Mail; -using Bit.Core.Models.StaticStore; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; @@ -200,221 +199,6 @@ public class OrganizationServiceTests referenceEvent.Users == expectedNewUsersCount)); } - [Theory] - [BitAutoData(PlanType.FamiliesAnnually)] - public async Task SignUp_PM_Family_Passes(PlanType planType, OrganizationSignup signup, SutProvider sutProvider) - { - signup.Plan = planType; - - var plan = StaticStore.GetPlan(signup.Plan); - - signup.AdditionalSeats = 0; - signup.PaymentMethodType = PaymentMethodType.Card; - signup.PremiumAccessAddon = false; - signup.UseSecretsManager = false; - signup.IsFromSecretsManagerTrial = false; - - var purchaseOrganizationPlan = StaticStore.GetPlan(signup.Plan); - - var result = await sutProvider.Sut.SignUpAsync(signup); - - await sutProvider.GetDependency().Received(1).CreateAsync( - Arg.Is(o => - o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats - && o.SmSeats == null - && o.SmServiceAccounts == null)); - await sutProvider.GetDependency().Received(1).CreateAsync( - Arg.Is(o => o.AccessSecretsManager == signup.UseSecretsManager)); - - await sutProvider.GetDependency().Received(1) - .RaiseEventAsync(Arg.Is(referenceEvent => - referenceEvent.Type == ReferenceEventType.Signup && - referenceEvent.PlanName == plan.Name && - referenceEvent.PlanType == plan.Type && - referenceEvent.Seats == result.Item1.Seats && - referenceEvent.Storage == result.Item1.MaxStorageGb)); - // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 - - Assert.NotNull(result.Item1); - Assert.NotNull(result.Item2); - - await sutProvider.GetDependency().Received(1).PurchaseOrganizationAsync( - Arg.Any(), - signup.PaymentMethodType.Value, - signup.PaymentToken, - plan, - signup.AdditionalStorageGb, - signup.AdditionalSeats, - signup.PremiumAccessAddon, - signup.TaxInfo, - false, - signup.AdditionalSmSeats.GetValueOrDefault(), - signup.AdditionalServiceAccounts.GetValueOrDefault(), - signup.UseSecretsManager - ); - } - - [Theory] - [BitAutoData(PlanType.FamiliesAnnually)] - public async Task SignUp_AssignsOwnerToDefaultCollection - (PlanType planType, OrganizationSignup signup, SutProvider sutProvider) - { - signup.Plan = planType; - signup.AdditionalSeats = 0; - signup.PaymentMethodType = PaymentMethodType.Card; - signup.PremiumAccessAddon = false; - signup.UseSecretsManager = false; - - // Extract orgUserId when created - Guid? orgUserId = null; - await sutProvider.GetDependency() - .CreateAsync(Arg.Do(ou => orgUserId = ou.Id)); - - var result = await sutProvider.Sut.SignUpAsync(signup); - - // Assert: created a Can Manage association for the default collection - Assert.NotNull(orgUserId); - await sutProvider.GetDependency().Received(1).CreateAsync( - Arg.Any(), - Arg.Is>(cas => cas == null), - Arg.Is>(cas => - cas.Count() == 1 && - cas.All(c => - c.Id == orgUserId && - !c.ReadOnly && - !c.HidePasswords && - c.Manage))); - - Assert.NotNull(result.Item1); - Assert.NotNull(result.Item2); - } - - [Theory] - [BitAutoData(PlanType.EnterpriseAnnually)] - [BitAutoData(PlanType.EnterpriseMonthly)] - [BitAutoData(PlanType.TeamsAnnually)] - [BitAutoData(PlanType.TeamsMonthly)] - public async Task SignUp_SM_Passes(PlanType planType, OrganizationSignup signup, SutProvider sutProvider) - { - signup.Plan = planType; - - var plan = StaticStore.GetPlan(signup.Plan); - - signup.UseSecretsManager = true; - signup.AdditionalSeats = 15; - signup.AdditionalSmSeats = 10; - signup.AdditionalServiceAccounts = 20; - signup.PaymentMethodType = PaymentMethodType.Card; - signup.PremiumAccessAddon = false; - signup.IsFromSecretsManagerTrial = false; - - var result = await sutProvider.Sut.SignUpAsync(signup); - - await sutProvider.GetDependency().Received(1).CreateAsync( - Arg.Is(o => - o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats - && o.SmSeats == plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats - && o.SmServiceAccounts == plan.SecretsManager.BaseServiceAccount + signup.AdditionalServiceAccounts)); - await sutProvider.GetDependency().Received(1).CreateAsync( - Arg.Is(o => o.AccessSecretsManager == signup.UseSecretsManager)); - - await sutProvider.GetDependency().Received(1) - .RaiseEventAsync(Arg.Is(referenceEvent => - referenceEvent.Type == ReferenceEventType.Signup && - referenceEvent.PlanName == plan.Name && - referenceEvent.PlanType == plan.Type && - referenceEvent.Seats == result.Item1.Seats && - referenceEvent.Storage == result.Item1.MaxStorageGb)); - // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 - - Assert.NotNull(result.Item1); - Assert.NotNull(result.Item2); - - await sutProvider.GetDependency().Received(1).PurchaseOrganizationAsync( - Arg.Any(), - signup.PaymentMethodType.Value, - signup.PaymentToken, - Arg.Is(plan), - signup.AdditionalStorageGb, - signup.AdditionalSeats, - signup.PremiumAccessAddon, - signup.TaxInfo, - false, - signup.AdditionalSmSeats.GetValueOrDefault(), - signup.AdditionalServiceAccounts.GetValueOrDefault(), - signup.IsFromSecretsManagerTrial - ); - } - - [Theory] - [BitAutoData(PlanType.EnterpriseAnnually)] - public async Task SignUp_SM_Throws_WhenManagedByMSP(PlanType planType, OrganizationSignup signup, SutProvider sutProvider) - { - signup.Plan = planType; - signup.UseSecretsManager = true; - signup.AdditionalSeats = 15; - signup.AdditionalSmSeats = 10; - signup.AdditionalServiceAccounts = 20; - signup.PaymentMethodType = PaymentMethodType.Card; - signup.PremiumAccessAddon = false; - signup.IsFromProvider = true; - - var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SignUpAsync(signup)); - Assert.Contains("Organizations with a Managed Service Provider do not support Secrets Manager.", exception.Message); - } - - [Theory] - [BitAutoData] - public async Task SignUpAsync_SecretManager_AdditionalServiceAccounts_NotAllowedByPlan_ShouldThrowException(OrganizationSignup signup, SutProvider sutProvider) - { - signup.AdditionalSmSeats = 0; - signup.AdditionalSeats = 0; - signup.Plan = PlanType.Free; - signup.UseSecretsManager = true; - signup.PaymentMethodType = PaymentMethodType.Card; - signup.PremiumAccessAddon = false; - signup.AdditionalServiceAccounts = 10; - signup.AdditionalStorageGb = 0; - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.SignUpAsync(signup)); - Assert.Contains("Plan does not allow additional Machine Accounts.", exception.Message); - } - - [Theory] - [BitAutoData] - public async Task SignUpAsync_SMSeatsGreatThanPMSeat_ShouldThrowException(OrganizationSignup signup, SutProvider sutProvider) - { - signup.AdditionalSmSeats = 100; - signup.AdditionalSeats = 10; - signup.Plan = PlanType.EnterpriseAnnually; - signup.UseSecretsManager = true; - signup.PaymentMethodType = PaymentMethodType.Card; - signup.PremiumAccessAddon = false; - signup.AdditionalServiceAccounts = 10; - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.SignUpAsync(signup)); - Assert.Contains("You cannot have more Secrets Manager seats than Password Manager seats", exception.Message); - } - - [Theory] - [BitAutoData] - public async Task SignUpAsync_InvalidateServiceAccount_ShouldThrowException(OrganizationSignup signup, SutProvider sutProvider) - { - signup.AdditionalSmSeats = 10; - signup.AdditionalSeats = 10; - signup.Plan = PlanType.EnterpriseAnnually; - signup.UseSecretsManager = true; - signup.PaymentMethodType = PaymentMethodType.Card; - signup.PremiumAccessAddon = false; - signup.AdditionalServiceAccounts = -10; - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.SignUpAsync(signup)); - Assert.Contains("You can't subtract Machine Accounts!", exception.Message); - } - [Theory, BitAutoData] public async Task SignupClientAsync_Succeeds( OrganizationSignup signup, From 092b0b8bd2ad8a335e5f5af52bab4046017517cc Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 6 Dec 2024 05:46:17 -0500 Subject: [PATCH 41/94] Remove `LimitCollectionCreationDeletionSplit` feature flag (#4809) * Remove references to feature flag * Demote entity property to an EF shadow property * Add a few excludes to license file tests --- .../Organizations/_ViewInformation.cshtml | 16 +- .../Controllers/OrganizationsController.cs | 8 - .../OrganizationResponseModel.cs | 4 - .../ProfileOrganizationResponseModel.cs | 4 - ...rofileProviderOrganizationResponseModel.cs | 2 - ...nCollectionManagementUpdateRequestModel.cs | 17 +- .../BulkCollectionAuthorizationHandler.cs | 54 +- .../AdminConsole/Entities/Organization.cs | 18 - .../Data/Organizations/OrganizationAbility.cs | 4 - .../OrganizationUserOrganizationDetails.cs | 2 - .../SelfHostedOrganizationDetails.cs | 2 - .../ProviderUserOrganizationDetails.cs | 1 - .../Implementations/OrganizationService.cs | 8 - .../OrganizationLicenseClaimsFactory.cs | 6 +- src/Core/Constants.cs | 2 +- .../Models/Business/OrganizationLicense.cs | 2 +- .../AdminConsole/Models/Organization.cs | 6 + .../Repositories/OrganizationRepository.cs | 4 +- ...izationUserOrganizationDetailsViewQuery.cs | 2 - ...roviderUserOrganizationDetailsViewQuery.cs | 2 - ...BulkCollectionAuthorizationHandlerTests.cs | 545 +----------------- .../OrganizationLicenseFileFixtures.cs | 3 +- .../UpdateOrganizationLicenseCommandTests.cs | 11 +- .../OrganizationUserRepositoryTests.cs | 2 - 24 files changed, 74 insertions(+), 651 deletions(-) diff --git a/src/Admin/AdminConsole/Views/Organizations/_ViewInformation.cshtml b/src/Admin/AdminConsole/Views/Organizations/_ViewInformation.cshtml index f3853e16a9..a0d421235d 100644 --- a/src/Admin/AdminConsole/Views/Organizations/_ViewInformation.cshtml +++ b/src/Admin/AdminConsole/Views/Organizations/_ViewInformation.cshtml @@ -55,19 +55,11 @@
Administrators manage all collections
@(Model.Organization.AllowAdminAccessToAllCollectionItems ? "On" : "Off")
- @if (!FeatureService.IsEnabled(Bit.Core.FeatureFlagKeys.LimitCollectionCreationDeletionSplit)) - { -
Limit collection creation to administrators
-
@(Model.Organization.LimitCollectionCreationDeletion ? "On" : "Off")
- } - else - { -
Limit collection creation to administrators
-
@(Model.Organization.LimitCollectionCreation ? "On" : "Off")
+
Limit collection creation to administrators
+
@(Model.Organization.LimitCollectionCreation ? "On" : "Off")
-
Limit collection deletion to administrators
-
@(Model.Organization.LimitCollectionDeletion ? "On" : "Off")
- } +
Limit collection deletion to administrators
+
@(Model.Organization.LimitCollectionDeletion ? "On" : "Off")

Secrets Manager

diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 6ffd60e425..4421af3a9a 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -526,14 +526,6 @@ public class OrganizationsController : Controller [HttpPut("{id}/collection-management")] public async Task PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model) { - if ( - _globalSettings.SelfHosted && - !_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit) - ) - { - throw new BadRequestException("Only allowed when not self hosted."); - } - var organization = await _organizationRepository.GetByIdAsync(id); if (organization == null) { diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs index 908a3a9385..116b4b1238 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs @@ -57,8 +57,6 @@ public class OrganizationResponseModel : ResponseModel MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts; LimitCollectionCreation = organization.LimitCollectionCreation; LimitCollectionDeletion = organization.LimitCollectionDeletion; - // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 - LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; UseRiskInsights = organization.UseRiskInsights; } @@ -104,8 +102,6 @@ public class OrganizationResponseModel : ResponseModel public int? MaxAutoscaleSmServiceAccounts { get; set; } public bool LimitCollectionCreation { get; set; } public bool LimitCollectionDeletion { get; set; } - // Deperectated: https://bitwarden.atlassian.net/browse/PM-10863 - public bool LimitCollectionCreationDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool UseRiskInsights { get; set; } } diff --git a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs index 96b86de164..75e4c44a6d 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs @@ -67,8 +67,6 @@ public class ProfileOrganizationResponseModel : ResponseModel AccessSecretsManager = organization.AccessSecretsManager; LimitCollectionCreation = organization.LimitCollectionCreation; LimitCollectionDeletion = organization.LimitCollectionDeletion; - // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 - LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId); UseRiskInsights = organization.UseRiskInsights; @@ -130,8 +128,6 @@ public class ProfileOrganizationResponseModel : ResponseModel public bool AccessSecretsManager { get; set; } public bool LimitCollectionCreation { get; set; } public bool LimitCollectionDeletion { get; set; } - // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 - public bool LimitCollectionCreationDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } /// /// Indicates if the organization manages the user. diff --git a/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs index 4ec86a29a4..7227d7a11a 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs @@ -46,8 +46,6 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier; LimitCollectionCreation = organization.LimitCollectionCreation; LimitCollectionDeletion = organization.LimitCollectionDeletion; - // https://bitwarden.atlassian.net/browse/PM-10863 - LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; UseRiskInsights = organization.UseRiskInsights; } diff --git a/src/Api/Models/Request/Organizations/OrganizationCollectionManagementUpdateRequestModel.cs b/src/Api/Models/Request/Organizations/OrganizationCollectionManagementUpdateRequestModel.cs index a5a6f1f74f..94f842ca1e 100644 --- a/src/Api/Models/Request/Organizations/OrganizationCollectionManagementUpdateRequestModel.cs +++ b/src/Api/Models/Request/Organizations/OrganizationCollectionManagementUpdateRequestModel.cs @@ -1,5 +1,4 @@ -using Bit.Core; -using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Services; namespace Bit.Api.Models.Request.Organizations; @@ -8,22 +7,12 @@ public class OrganizationCollectionManagementUpdateRequestModel { public bool LimitCollectionCreation { get; set; } public bool LimitCollectionDeletion { get; set; } - // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 - public bool LimitCreateDeleteOwnerAdmin { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } public virtual Organization ToOrganization(Organization existingOrganization, IFeatureService featureService) { - if (featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)) - { - existingOrganization.LimitCollectionCreation = LimitCollectionCreation; - existingOrganization.LimitCollectionDeletion = LimitCollectionDeletion; - } - else - { - existingOrganization.LimitCollectionCreationDeletion = LimitCreateDeleteOwnerAdmin || LimitCollectionCreation || LimitCollectionDeletion; - } - + existingOrganization.LimitCollectionCreation = LimitCollectionCreation; + existingOrganization.LimitCollectionDeletion = LimitCollectionDeletion; existingOrganization.AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems; return existingOrganization; } diff --git a/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionAuthorizationHandler.cs b/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionAuthorizationHandler.cs index c26d5b5952..909064c522 100644 --- a/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionAuthorizationHandler.cs +++ b/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionAuthorizationHandler.cs @@ -1,6 +1,5 @@ #nullable enable using System.Diagnostics; -using Bit.Core; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -124,24 +123,15 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler, IStorableSubscriber, IRevisable, /// public bool LimitCollectionCreation { get; set; } public bool LimitCollectionDeletion { get; set; } - // Deprecated by https://bitwarden.atlassian.net/browse/PM-10863. This - // was replaced with `LimitCollectionCreation` and - // `LimitCollectionDeletion`. - public bool LimitCollectionCreationDeletion - { - get => LimitCollectionCreation || LimitCollectionDeletion; - set - { - LimitCollectionCreation = value; - LimitCollectionDeletion = value; - } - } /// /// If set to true, admins, owners, and some custom users can read/write all collections and items in the Admin Console. @@ -319,11 +307,5 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable, UseSecretsManager = license.UseSecretsManager; SmSeats = license.SmSeats; SmServiceAccounts = license.SmServiceAccounts; - - if (!featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)) - { - LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion; - AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems; - } } } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs index 0da0928dbe..6392e483ce 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs @@ -23,8 +23,6 @@ public class OrganizationAbility UsePolicies = organization.UsePolicies; LimitCollectionCreation = organization.LimitCollectionCreation; LimitCollectionDeletion = organization.LimitCollectionDeletion; - // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 - LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; UseRiskInsights = organization.UseRiskInsights; } @@ -43,8 +41,6 @@ public class OrganizationAbility public bool UsePolicies { get; set; } public bool LimitCollectionCreation { get; set; } public bool LimitCollectionDeletion { get; set; } - // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 - public bool LimitCollectionCreationDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool UseRiskInsights { get; set; } } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs index 7b9ea971d3..e06b6bd66a 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs @@ -56,8 +56,6 @@ public class OrganizationUserOrganizationDetails public int? SmServiceAccounts { get; set; } public bool LimitCollectionCreation { get; set; } public bool LimitCollectionDeletion { get; set; } - // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 - public bool LimitCollectionCreationDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool UseRiskInsights { get; set; } } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs index 1fa547d98b..bd727f707b 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs @@ -146,8 +146,6 @@ public class SelfHostedOrganizationDetails : Organization OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling, LimitCollectionCreation = LimitCollectionCreation, LimitCollectionDeletion = LimitCollectionDeletion, - // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 - LimitCollectionCreationDeletion = LimitCollectionCreationDeletion, AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems, Status = Status }; diff --git a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs index c2880b543f..f37cc644d4 100644 --- a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs @@ -42,7 +42,6 @@ public class ProviderUserOrganizationDetails public PlanType PlanType { get; set; } public bool LimitCollectionCreation { get; set; } public bool LimitCollectionDeletion { get; set; } - public bool LimitCollectionCreationDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool UseRiskInsights { get; set; } } diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 1ca047aa47..eebe76baef 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -581,14 +581,6 @@ public class OrganizationService : IOrganizationService SmServiceAccounts = license.SmServiceAccounts, }; - // These fields are being removed from consideration when processing - // licenses. - if (!_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)) - { - organization.LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion; - organization.AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems; - } - var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false); var dir = $"{_globalSettings.LicenseDirectory}/organization"; diff --git a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs index 300b87dcca..1aac7bb1d8 100644 --- a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs +++ b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs @@ -52,7 +52,11 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory Ciphers { get; set; } public virtual ICollection OrganizationUsers { get; set; } public virtual ICollection Groups { get; set; } @@ -38,6 +43,7 @@ public class OrganizationMapperProfile : Profile .ForMember(org => org.ApiKeys, opt => opt.Ignore()) .ForMember(org => org.Connections, opt => opt.Ignore()) .ForMember(org => org.Domains, opt => opt.Ignore()) + .ForMember(org => org.LimitCollectionCreationDeletion, opt => opt.Ignore()) .ReverseMap(); CreateProjection() diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index 7a667db8f5..fb3766c6c7 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -101,10 +101,8 @@ public class OrganizationRepository : Repository().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.True(context.HasSucceeded); } [Theory, BitAutoData, CollectionCustomization] - public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationFalse_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success( + public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationFalse_Success( SutProvider sutProvider, ICollection collections, CurrentContextOrganization organization) @@ -62,7 +57,7 @@ public class BulkCollectionAuthorizationHandlerTests organization.Type = OrganizationUserType.User; - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false); + ArrangeOrganizationAbility(sutProvider, organization, false, false); var context = new AuthorizationHandlerContext( new[] { BulkCollectionOperations.Create }, @@ -71,49 +66,16 @@ public class BulkCollectionAuthorizationHandlerTests sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit) - .Returns(false); await sutProvider.Sut.HandleAsync(context); - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); - Assert.True(context.HasSucceeded); - } - - [Theory, BitAutoData, CollectionCustomization] - public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationFalse_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success( - SutProvider sutProvider, - ICollection collections, - CurrentContextOrganization organization) - { - var actingUserId = Guid.NewGuid(); - - organization.Type = OrganizationUserType.User; - - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false); - - var context = new AuthorizationHandlerContext( - new[] { BulkCollectionOperations.Create }, - new ClaimsPrincipal(), - collections); - - sutProvider.GetDependency().UserId.Returns(actingUserId); - sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit) - .Returns(true); - - await sutProvider.Sut.HandleAsync(context); - - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.True(context.HasSucceeded); } [Theory, CollectionCustomization] [BitAutoData(OrganizationUserType.User)] [BitAutoData(OrganizationUserType.Custom)] - public async Task CanCreateAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess( + public async Task CanCreateAsync_WhenMissingPermissions_NoSuccess( OrganizationUserType userType, SutProvider sutProvider, ICollection collections, @@ -130,7 +92,7 @@ public class BulkCollectionAuthorizationHandlerTests ManageUsers = false }; - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true); + ArrangeOrganizationAbility(sutProvider, organization, true, true); var context = new AuthorizationHandlerContext( new[] { BulkCollectionOperations.Create }, @@ -140,61 +102,21 @@ public class BulkCollectionAuthorizationHandlerTests sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); await sutProvider.Sut.HandleAsync(context); - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); - Assert.False(context.HasSucceeded); - } - - [Theory, CollectionCustomization] - [BitAutoData(OrganizationUserType.User)] - [BitAutoData(OrganizationUserType.Custom)] - public async Task CanCreateAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess( - OrganizationUserType userType, - SutProvider sutProvider, - ICollection collections, - CurrentContextOrganization organization) - { - var actingUserId = Guid.NewGuid(); - - organization.Type = userType; - organization.Permissions = new Permissions - { - EditAnyCollection = false, - DeleteAnyCollection = false, - ManageGroups = false, - ManageUsers = false - }; - - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true); - - var context = new AuthorizationHandlerContext( - new[] { BulkCollectionOperations.Create }, - new ClaimsPrincipal(), - collections); - - sutProvider.GetDependency().UserId.Returns(actingUserId); - sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); - - await sutProvider.Sut.HandleAsync(context); - - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.False(context.HasSucceeded); } [Theory, BitAutoData, CollectionCustomization] - public async Task CanCreateAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitDisabled_NoSuccess( + public async Task CanCreateAsync_WhenMissingOrgAccess_NoSuccess( Guid userId, CurrentContextOrganization organization, List collections, SutProvider sutProvider) { collections.ForEach(c => c.OrganizationId = organization.Id); - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true); + ArrangeOrganizationAbility(sutProvider, organization, true, true); var context = new AuthorizationHandlerContext( new[] { BulkCollectionOperations.Create }, @@ -205,38 +127,9 @@ public class BulkCollectionAuthorizationHandlerTests sutProvider.GetDependency().UserId.Returns(userId); sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); await sutProvider.Sut.HandleAsync(context); - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); - Assert.False(context.HasSucceeded); - } - - [Theory, BitAutoData, CollectionCustomization] - public async Task CanCreateAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitEnabled_NoSuccess( - Guid userId, - CurrentContextOrganization organization, - List collections, - SutProvider sutProvider) - { - collections.ForEach(c => c.OrganizationId = organization.Id); - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true); - - var context = new AuthorizationHandlerContext( - new[] { BulkCollectionOperations.Create }, - new ClaimsPrincipal(), - collections - ); - - sutProvider.GetDependency().UserId.Returns(userId); - sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); - sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); - - await sutProvider.Sut.HandleAsync(context); - - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.False(context.HasSucceeded); } @@ -1015,7 +908,7 @@ public class BulkCollectionAuthorizationHandlerTests // `LimitCollectonCreationDeletionSplit` feature flag state isn't // relevant for this test. The flag is never checked for in this // test. This is asserted below. - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true); + ArrangeOrganizationAbility(sutProvider, organization, true, true); var context = new AuthorizationHandlerContext( new[] { BulkCollectionOperations.Delete }, @@ -1027,7 +920,6 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); - sutProvider.GetDependency().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.True(context.HasSucceeded); } @@ -1046,7 +938,7 @@ public class BulkCollectionAuthorizationHandlerTests // `LimitCollectonCreationDeletionSplit` feature flag state isn't // relevant for this test. The flag is never checked for in this // test. This is asserted below. - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true); + ArrangeOrganizationAbility(sutProvider, organization, true, true); var context = new AuthorizationHandlerContext( new[] { BulkCollectionOperations.Delete }, @@ -1058,12 +950,11 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); - sutProvider.GetDependency().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.True(context.HasSucceeded); } [Theory, BitAutoData, CollectionCustomization] - public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success( + public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionFalse_WithCanManagePermission_Success( SutProvider sutProvider, ICollection collections, CurrentContextOrganization organization) @@ -1073,12 +964,11 @@ public class BulkCollectionAuthorizationHandlerTests organization.Type = OrganizationUserType.User; organization.Permissions = new Permissions(); - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false); + ArrangeOrganizationAbility(sutProvider, organization, false, false); sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); foreach (var c in collections) { @@ -1092,41 +982,6 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); - Assert.True(context.HasSucceeded); - } - - [Theory, BitAutoData, CollectionCustomization] - public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success( - SutProvider sutProvider, - ICollection collections, - CurrentContextOrganization organization) - { - var actingUserId = Guid.NewGuid(); - - organization.Type = OrganizationUserType.User; - organization.Permissions = new Permissions(); - - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false); - - sutProvider.GetDependency().UserId.Returns(actingUserId); - sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); - - foreach (var c in collections) - { - c.Manage = true; - } - - var context = new AuthorizationHandlerContext( - new[] { BulkCollectionOperations.Delete }, - new ClaimsPrincipal(), - collections); - - await sutProvider.Sut.HandleAsync(context); - - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.True(context.HasSucceeded); } @@ -1134,7 +989,7 @@ public class BulkCollectionAuthorizationHandlerTests [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.User)] - public async Task CanDeleteAsync_LimitCollectionDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success( + public async Task CanDeleteAsync_LimitCollectionDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success( OrganizationUserType userType, SutProvider sutProvider, ICollection collections, @@ -1145,12 +1000,11 @@ public class BulkCollectionAuthorizationHandlerTests organization.Type = userType; organization.Permissions = new Permissions(); - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false, false); + ArrangeOrganizationAbility(sutProvider, organization, false, false, false); sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); foreach (var c in collections) { @@ -1164,15 +1018,13 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.True(context.HasSucceeded); } [Theory, CollectionCustomization] [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Owner)] - [BitAutoData(OrganizationUserType.User)] - public async Task CanDeleteAsync_LimitCollectionDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success( + public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success( OrganizationUserType userType, SutProvider sutProvider, ICollection collections, @@ -1183,12 +1035,11 @@ public class BulkCollectionAuthorizationHandlerTests organization.Type = userType; organization.Permissions = new Permissions(); - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false, false); + ArrangeOrganizationAbility(sutProvider, organization, true, true, false); sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); foreach (var c in collections) { @@ -1202,14 +1053,13 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.True(context.HasSucceeded); } [Theory, CollectionCustomization] [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Owner)] - public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success( + public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_Failure( OrganizationUserType userType, SutProvider sutProvider, ICollection collections, @@ -1220,87 +1070,12 @@ public class BulkCollectionAuthorizationHandlerTests organization.Type = userType; organization.Permissions = new Permissions(); - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, false); - - sutProvider.GetDependency().UserId.Returns(actingUserId); - sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); - - foreach (var c in collections) - { - c.Manage = true; - } - - var context = new AuthorizationHandlerContext( - new[] { BulkCollectionOperations.Delete }, - new ClaimsPrincipal(), - collections); - - await sutProvider.Sut.HandleAsync(context); - - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); - Assert.True(context.HasSucceeded); - } - - [Theory, CollectionCustomization] - [BitAutoData(OrganizationUserType.Admin)] - [BitAutoData(OrganizationUserType.Owner)] - public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success( - OrganizationUserType userType, - SutProvider sutProvider, - ICollection collections, - CurrentContextOrganization organization) - { - var actingUserId = Guid.NewGuid(); - - organization.Type = userType; - organization.Permissions = new Permissions(); - - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false); - - sutProvider.GetDependency().UserId.Returns(actingUserId); - sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); - - foreach (var c in collections) - { - c.Manage = true; - } - - var context = new AuthorizationHandlerContext( - new[] { BulkCollectionOperations.Delete }, - new ClaimsPrincipal(), - collections); - - await sutProvider.Sut.HandleAsync(context); - - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); - Assert.True(context.HasSucceeded); - } - - [Theory, CollectionCustomization] - [BitAutoData(OrganizationUserType.Admin)] - [BitAutoData(OrganizationUserType.Owner)] - public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure( - OrganizationUserType userType, - SutProvider sutProvider, - ICollection collections, - CurrentContextOrganization organization) - { - var actingUserId = Guid.NewGuid(); - - organization.Type = userType; - organization.Permissions = new Permissions(); - - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, false); + ArrangeOrganizationAbility(sutProvider, organization, true, true, false); sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); foreach (var c in collections) { @@ -1314,50 +1089,11 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); - Assert.False(context.HasSucceeded); - } - - [Theory, CollectionCustomization] - [BitAutoData(OrganizationUserType.Admin)] - [BitAutoData(OrganizationUserType.Owner)] - public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure( - OrganizationUserType userType, - SutProvider sutProvider, - ICollection collections, - CurrentContextOrganization organization) - { - var actingUserId = Guid.NewGuid(); - - organization.Type = userType; - organization.Permissions = new Permissions(); - - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false); - - sutProvider.GetDependency().UserId.Returns(actingUserId); - sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); - sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); - - foreach (var c in collections) - { - c.Manage = false; - } - - var context = new AuthorizationHandlerContext( - new[] { BulkCollectionOperations.Delete }, - new ClaimsPrincipal(), - collections); - - await sutProvider.Sut.HandleAsync(context); - - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.False(context.HasSucceeded); } [Theory, BitAutoData, CollectionCustomization] - public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure( + public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_Failure( SutProvider sutProvider, ICollection collections, CurrentContextOrganization organization) @@ -1367,13 +1103,12 @@ public class BulkCollectionAuthorizationHandlerTests organization.Type = OrganizationUserType.User; organization.Permissions = new Permissions(); - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true); + ArrangeOrganizationAbility(sutProvider, organization, true, true); sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); foreach (var c in collections) { @@ -1387,12 +1122,11 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.False(context.HasSucceeded); } [Theory, BitAutoData, CollectionCustomization] - public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure( + public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_Failure( SutProvider sutProvider, ICollection collections, CurrentContextOrganization organization) @@ -1402,13 +1136,12 @@ public class BulkCollectionAuthorizationHandlerTests organization.Type = OrganizationUserType.User; organization.Permissions = new Permissions(); - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true); + ArrangeOrganizationAbility(sutProvider, organization, true, true, false); sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); foreach (var c in collections) { @@ -1422,88 +1155,13 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); - Assert.False(context.HasSucceeded); - } - - [Theory, BitAutoData, CollectionCustomization] - public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure( - SutProvider sutProvider, - ICollection collections, - CurrentContextOrganization organization) - { - var actingUserId = Guid.NewGuid(); - - organization.Type = OrganizationUserType.User; - organization.Permissions = new Permissions(); - - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, false); - - sutProvider.GetDependency().UserId.Returns(actingUserId); - sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); - sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit) - .Returns(false); - - foreach (var c in collections) - { - c.Manage = true; - } - - var context = new AuthorizationHandlerContext( - new[] { BulkCollectionOperations.Delete }, - new ClaimsPrincipal(), - collections); - - await sutProvider.Sut.HandleAsync(context); - - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); - Assert.False(context.HasSucceeded); - } - - [Theory, BitAutoData, CollectionCustomization] - public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure( - SutProvider sutProvider, - ICollection collections, - CurrentContextOrganization organization) - { - var actingUserId = Guid.NewGuid(); - - organization.Type = OrganizationUserType.User; - organization.Permissions = new Permissions(); - - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false); - - sutProvider.GetDependency().UserId.Returns(actingUserId); - sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); - sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit) - .Returns(true); - - foreach (var c in collections) - { - c.Manage = true; - } - - var context = new AuthorizationHandlerContext( - new[] { BulkCollectionOperations.Delete }, - new ClaimsPrincipal(), - collections); - - await sutProvider.Sut.HandleAsync(context); - - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.False(context.HasSucceeded); } [Theory, CollectionCustomization] [BitAutoData(OrganizationUserType.User)] [BitAutoData(OrganizationUserType.Custom)] - public async Task CanDeleteAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess( + public async Task CanDeleteAsync_WhenMissingPermissions_NoSuccess( OrganizationUserType userType, SutProvider sutProvider, ICollection collections, @@ -1520,7 +1178,7 @@ public class BulkCollectionAuthorizationHandlerTests ManageUsers = false }; - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true); + ArrangeOrganizationAbility(sutProvider, organization, true, true); var context = new AuthorizationHandlerContext( new[] { BulkCollectionOperations.Delete }, @@ -1530,54 +1188,14 @@ public class BulkCollectionAuthorizationHandlerTests sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); await sutProvider.Sut.HandleAsync(context); - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); - Assert.False(context.HasSucceeded); - } - - [Theory, CollectionCustomization] - [BitAutoData(OrganizationUserType.User)] - [BitAutoData(OrganizationUserType.Custom)] - public async Task CanDeleteAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess( - OrganizationUserType userType, - SutProvider sutProvider, - ICollection collections, - CurrentContextOrganization organization) - { - var actingUserId = Guid.NewGuid(); - - organization.Type = userType; - organization.Permissions = new Permissions - { - EditAnyCollection = false, - DeleteAnyCollection = false, - ManageGroups = false, - ManageUsers = false - }; - - ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true); - - var context = new AuthorizationHandlerContext( - new[] { BulkCollectionOperations.Delete }, - new ClaimsPrincipal(), - collections); - - sutProvider.GetDependency().UserId.Returns(actingUserId); - sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); - - await sutProvider.Sut.HandleAsync(context); - - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.False(context.HasSucceeded); } [Theory, BitAutoData, CollectionCustomization] - public async Task CanDeleteAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess( + public async Task CanDeleteAsync_WhenMissingOrgAccess_NoSuccess( Guid userId, ICollection collections, SutProvider sutProvider) @@ -1591,34 +1209,9 @@ public class BulkCollectionAuthorizationHandlerTests sutProvider.GetDependency().UserId.Returns(userId); sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); await sutProvider.Sut.HandleAsync(context); - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); - Assert.False(context.HasSucceeded); - } - - [Theory, BitAutoData, CollectionCustomization] - public async Task CanDeleteAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess( - Guid userId, - ICollection collections, - SutProvider sutProvider) - { - var context = new AuthorizationHandlerContext( - new[] { BulkCollectionOperations.Delete }, - new ClaimsPrincipal(), - collections - ); - - sutProvider.GetDependency().UserId.Returns(userId); - sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); - sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); - - await sutProvider.Sut.HandleAsync(context); - - sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.False(context.HasSucceeded); } @@ -1639,7 +1232,6 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); Assert.True(context.HasFailed); sutProvider.GetDependency().DidNotReceiveWithAnyArgs(); - sutProvider.GetDependency().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); } [Theory, BitAutoData, CollectionCustomization] @@ -1663,66 +1255,10 @@ public class BulkCollectionAuthorizationHandlerTests var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.HandleAsync(context)); Assert.Equal("Requested collections must belong to the same organization.", exception.Message); sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetOrganization(default); - sutProvider.GetDependency().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); } [Theory, BitAutoData, CollectionCustomization] - public async Task HandleRequirementAsync_Provider_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success( - SutProvider sutProvider, - ICollection collections) - { - var actingUserId = Guid.NewGuid(); - var orgId = collections.First().OrganizationId; - - var organizationAbilities = new Dictionary - { - { collections.First().OrganizationId, - new OrganizationAbility - { - LimitCollectionCreationDeletion = true, - AllowAdminAccessToAllCollectionItems = true - } - } - }; - - var operationsToTest = new[] - { - BulkCollectionOperations.Create, - BulkCollectionOperations.Read, - BulkCollectionOperations.ReadAccess, - BulkCollectionOperations.Update, - BulkCollectionOperations.ModifyUserAccess, - BulkCollectionOperations.ModifyGroupAccess, - BulkCollectionOperations.Delete, - }; - - foreach (var op in operationsToTest) - { - sutProvider.GetDependency().UserId.Returns(actingUserId); - sutProvider.GetDependency().GetOrganization(orgId).Returns((CurrentContextOrganization)null); - sutProvider.GetDependency().GetOrganizationAbilitiesAsync() - .Returns(organizationAbilities); - sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(true); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); - - var context = new AuthorizationHandlerContext( - new[] { op }, - new ClaimsPrincipal(), - collections - ); - - await sutProvider.Sut.HandleAsync(context); - - Assert.True(context.HasSucceeded); - await sutProvider.GetDependency().Received().ProviderUserForOrgAsync(orgId); - - // Recreate the SUT to reset the mocks/dependencies between tests - sutProvider.Recreate(); - } - } - - [Theory, BitAutoData, CollectionCustomization] - public async Task HandleRequirementAsync_Provider_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success( + public async Task HandleRequirementAsync_Provider_Success( SutProvider sutProvider, ICollection collections) { @@ -1759,7 +1295,6 @@ public class BulkCollectionAuthorizationHandlerTests sutProvider.GetDependency().GetOrganizationAbilitiesAsync() .Returns(organizationAbilities); sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(true); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); var context = new AuthorizationHandlerContext( new[] { op }, @@ -1810,30 +1345,12 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.GetDependency().Received(1).GetManyByUserIdAsync(Arg.Any()); } - private static void ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled( - SutProvider sutProvider, - CurrentContextOrganization organization, - bool limitCollectionCreation, - bool limitCollectionDeletion, - bool allowAdminAccessToAllCollectionItems = true) - { - var organizationAbility = new OrganizationAbility(); - organizationAbility.Id = organization.Id; - - organizationAbility.LimitCollectionCreationDeletion = limitCollectionCreation || limitCollectionDeletion; - - organizationAbility.AllowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems; - - sutProvider.GetDependency().GetOrganizationAbilityAsync(organizationAbility.Id) - .Returns(organizationAbility); - } - - private static void ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled( - SutProvider sutProvider, - CurrentContextOrganization organization, - bool limitCollectionCreation, - bool limitCollectionDeletion, - bool allowAdminAccessToAllCollectionItems = true) + private static void ArrangeOrganizationAbility( + SutProvider sutProvider, + CurrentContextOrganization organization, + bool limitCollectionCreation, + bool limitCollectionDeletion, + bool allowAdminAccessToAllCollectionItems = true) { var organizationAbility = new OrganizationAbility(); organizationAbility.Id = organization.Id; diff --git a/test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs b/test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs index 500c4475a9..de5fb25fca 100644 --- a/test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs +++ b/test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs @@ -111,7 +111,8 @@ public static class OrganizationLicenseFileFixtures SmServiceAccounts = 8, MaxAutoscaleSmSeats = 101, MaxAutoscaleSmServiceAccounts = 102, - LimitCollectionCreationDeletion = true, + LimitCollectionCreation = true, + LimitCollectionDeletion = true, AllowAdminAccessToAllCollectionItems = true, }; } diff --git a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs index b8e677177c..420d330aaa 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs @@ -83,11 +83,12 @@ public class UpdateOrganizationLicenseCommandTests .Received(1) .ReplaceAndUpdateCacheAsync(Arg.Is( org => AssertPropertyEqual(license, org, - "Id", "MaxStorageGb", "Issued", "Refresh", "Version", "Trial", "LicenseType", - "Hash", "Signature", "SignatureBytes", "InstallationId", "Expires", - "ExpirationWithoutGracePeriod", "Token") && - // Same property but different name, use explicit mapping - org.ExpirationDate == license.Expires)); + "Id", "MaxStorageGb", "Issued", "Refresh", "Version", "Trial", "LicenseType", + "Hash", "Signature", "SignatureBytes", "InstallationId", "Expires", + "ExpirationWithoutGracePeriod", "Token", "LimitCollectionCreationDeletion", + "LimitCollectionCreation", "LimitCollectionDeletion", "AllowAdminAccessToAllCollectionItems") && + // Same property but different name, use explicit mapping + org.ExpirationDate == license.Expires)); } finally { diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs index 4732d8a474..aee4beb8ce 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs @@ -255,8 +255,6 @@ public class OrganizationUserRepositoryTests Assert.Equal(organization.SmServiceAccounts, result.SmServiceAccounts); Assert.Equal(organization.LimitCollectionCreation, result.LimitCollectionCreation); Assert.Equal(organization.LimitCollectionDeletion, result.LimitCollectionDeletion); - // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 - Assert.Equal(organization.LimitCollectionCreationDeletion, result.LimitCollectionCreationDeletion); Assert.Equal(organization.AllowAdminAccessToAllCollectionItems, result.AllowAdminAccessToAllCollectionItems); Assert.Equal(organization.UseRiskInsights, result.UseRiskInsights); } From 9ebddd223acc6e0408e30caf6d24213873369b69 Mon Sep 17 00:00:00 2001 From: Opeyemi Date: Fri, 6 Dec 2024 16:53:52 +0000 Subject: [PATCH 42/94] [BRE-470] - Update Renovate Conf for BRE team (#5123) --- .github/renovate.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index ac08134041..5779b28edb 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -26,7 +26,7 @@ }, { "matchManagers": ["github-actions", "dockerfile", "docker-compose"], - "commitMessagePrefix": "[deps] DevOps:" + "commitMessagePrefix": "[deps] BRE:" }, { "matchPackageNames": ["DnsClient"], @@ -116,8 +116,8 @@ { "matchPackageNames": ["CommandDotNet", "YamlDotNet"], "description": "DevOps owned dependencies", - "commitMessagePrefix": "[deps] DevOps:", - "reviewers": ["team:dept-devops"] + "commitMessagePrefix": "[deps] BRE:", + "reviewers": ["team:dept-bre"] }, { "matchPackageNames": [ From fb5db40f4c8fa6b2324c85280935525ae827f03e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=9F=E6=AD=A6=2E=E5=B0=BC=E5=BE=B7=E9=9C=8D=E6=A0=BC?= =?UTF-8?q?=2E=E9=BE=8D?= <7708801314520.tony@gmail.com> Date: Sat, 7 Dec 2024 02:34:50 +0800 Subject: [PATCH 43/94] Update docker reference link (#5096) Update docker reference link Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- util/Setup/Templates/DockerCompose.hbs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/util/Setup/Templates/DockerCompose.hbs b/util/Setup/Templates/DockerCompose.hbs index d9ad6c4613..ffe9121089 100644 --- a/util/Setup/Templates/DockerCompose.hbs +++ b/util/Setup/Templates/DockerCompose.hbs @@ -1,8 +1,8 @@ # # Useful references: -# https://docs.docker.com/compose/compose-file/ -# https://docs.docker.com/compose/reference/overview/#use--f-to-specify-name-and-path-of-one-or-more-compose-files -# https://docs.docker.com/compose/reference/envvars/ +# https://docs.docker.com/reference/compose-file/ +# https://docs.docker.com/reference/cli/docker/compose/#use--f-to-specify-the-name-and-path-of-one-or-more-compose-files +# https://docs.docker.com/compose/how-tos/environment-variables/envvars/ # ######################################################################### # WARNING: This file is generated. Do not make changes to this file. # From c591997d0136a6126308444068cfa89f604c531f Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Fri, 6 Dec 2024 14:40:47 -0500 Subject: [PATCH 44/94] [PM-13013] add delete many async method to i user repository and i user service for bulk user deletion (#5035) * Add DeleteManyAsync method and stored procedure * Add DeleteManyAsync and tests * removed stored procedure, refactor User_DeleteById to accept multiple Ids * add sproc, refactor tests * revert existing sproc * add bulk delete to IUserService * fix sproc * fix and add tests * add migration script, fix test * Add feature flag * add feature flag to tests for deleteManyAsync * enable nullable, delete only user that pass validation * revert changes to DeleteAsync * Cleanup whitespace * remove redundant feature flag * fix tests * move DeleteManyAsync from UserService into DeleteManagedOrganizationUserAccountCommand * refactor validation, remove unneeded tasks * refactor tests, remove unused service --- ...teManagedOrganizationUserAccountCommand.cs | 86 +++++++++- src/Core/Repositories/IUserRepository.cs | 1 + .../Repositories/UserRepository.cs | 12 ++ .../Repositories/UserRepository.cs | 47 ++++++ .../Stored Procedures/User_DeleteByIds.sql | 158 ++++++++++++++++++ ...agedOrganizationUserAccountCommandTests.cs | 12 +- .../Auth/Repositories/UserRepositoryTests.cs | 99 +++++++++++ .../2024-11-22_00_UserDeleteByIds.sql | 158 ++++++++++++++++++ 8 files changed, 565 insertions(+), 8 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/User_DeleteByIds.sql create mode 100644 test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs create mode 100644 util/Migrator/DbScripts/2024-11-22_00_UserDeleteByIds.sql diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommand.cs index 0bcd16cee1..cb7e2a6250 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommand.cs @@ -1,10 +1,14 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Tools.Services; #nullable enable @@ -19,7 +23,10 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz private readonly IUserRepository _userRepository; private readonly ICurrentContext _currentContext; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; - + private readonly IReferenceEventService _referenceEventService; + private readonly IPushNotificationService _pushService; + private readonly IOrganizationRepository _organizationRepository; + private readonly IProviderUserRepository _providerUserRepository; public DeleteManagedOrganizationUserAccountCommand( IUserService userService, IEventService eventService, @@ -27,7 +34,11 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz IOrganizationUserRepository organizationUserRepository, IUserRepository userRepository, ICurrentContext currentContext, - IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, + IReferenceEventService referenceEventService, + IPushNotificationService pushService, + IOrganizationRepository organizationRepository, + IProviderUserRepository providerUserRepository) { _userService = userService; _eventService = eventService; @@ -36,6 +47,10 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz _userRepository = userRepository; _currentContext = currentContext; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; + _referenceEventService = referenceEventService; + _pushService = pushService; + _organizationRepository = organizationRepository; + _providerUserRepository = providerUserRepository; } public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId) @@ -89,7 +104,8 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz throw new NotFoundException("Member not found."); } - await _userService.DeleteAsync(user); + await ValidateUserMembershipAndPremiumAsync(user); + results.Add((orgUserId, string.Empty)); } catch (Exception ex) @@ -98,6 +114,15 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz } } + var orgUserResultsToDelete = results.Where(result => string.IsNullOrEmpty(result.ErrorMessage)); + var orgUsersToDelete = orgUsers.Where(orgUser => orgUserResultsToDelete.Any(result => orgUser.Id == result.OrganizationUserId)); + var usersToDelete = users.Where(user => orgUsersToDelete.Any(orgUser => orgUser.UserId == user.Id)); + + if (usersToDelete.Any()) + { + await DeleteManyAsync(usersToDelete); + } + await LogDeletedOrganizationUsersAsync(orgUsers, results); return results; @@ -158,4 +183,59 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz await _eventService.LogOrganizationUserEventsAsync(events); } } + private async Task DeleteManyAsync(IEnumerable users) + { + + await _userRepository.DeleteManyAsync(users); + foreach (var user in users) + { + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.DeleteAccount, user, _currentContext)); + await _pushService.PushLogOutAsync(user.Id); + } + + } + + private async Task ValidateUserMembershipAndPremiumAsync(User user) + { + // Check if user is the only owner of any organizations. + var onlyOwnerCount = await _organizationUserRepository.GetCountByOnlyOwnerAsync(user.Id); + if (onlyOwnerCount > 0) + { + throw new BadRequestException("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user."); + } + + var orgs = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed); + if (orgs.Count == 1) + { + var org = await _organizationRepository.GetByIdAsync(orgs.First().OrganizationId); + if (org != null && (!org.Enabled || string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))) + { + var orgCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(org.Id); + if (orgCount <= 1) + { + await _organizationRepository.DeleteAsync(org); + } + else + { + throw new BadRequestException("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user."); + } + } + } + + var onlyOwnerProviderCount = await _providerUserRepository.GetCountByOnlyOwnerAsync(user.Id); + if (onlyOwnerProviderCount > 0) + { + throw new BadRequestException("Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user."); + } + + if (!string.IsNullOrWhiteSpace(user.GatewaySubscriptionId)) + { + try + { + await _userService.CancelPremiumAsync(user); + } + catch (GatewayException) { } + } + } } diff --git a/src/Core/Repositories/IUserRepository.cs b/src/Core/Repositories/IUserRepository.cs index 22e2ec1a07..040e6e1f49 100644 --- a/src/Core/Repositories/IUserRepository.cs +++ b/src/Core/Repositories/IUserRepository.cs @@ -32,4 +32,5 @@ public interface IUserRepository : IRepository /// Registered database calls to update re-encrypted data. Task UpdateUserKeyAndEncryptedDataAsync(User user, IEnumerable updateDataActions); + Task DeleteManyAsync(IEnumerable users); } diff --git a/src/Infrastructure.Dapper/Repositories/UserRepository.cs b/src/Infrastructure.Dapper/Repositories/UserRepository.cs index 9e613fdf08..227a7c03e5 100644 --- a/src/Infrastructure.Dapper/Repositories/UserRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/UserRepository.cs @@ -172,6 +172,18 @@ public class UserRepository : Repository, IUserRepository commandTimeout: 180); } } + public async Task DeleteManyAsync(IEnumerable users) + { + var ids = users.Select(user => user.Id); + using (var connection = new SqlConnection(ConnectionString)) + { + await connection.ExecuteAsync( + $"[{Schema}].[{Table}_DeleteByIds]", + new { Ids = JsonSerializer.Serialize(ids) }, + commandType: CommandType.StoredProcedure, + commandTimeout: 180); + } + } public async Task UpdateStorageAsync(Guid id) { diff --git a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs index d234d25455..cbfefb6483 100644 --- a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs @@ -261,6 +261,53 @@ public class UserRepository : Repository, IUserR var mappedUser = Mapper.Map(user); dbContext.Users.Remove(mappedUser); + await transaction.CommitAsync(); + await dbContext.SaveChangesAsync(); + } + } + + public async Task DeleteManyAsync(IEnumerable users) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + + var transaction = await dbContext.Database.BeginTransactionAsync(); + + var targetIds = users.Select(u => u.Id).ToList(); + + await dbContext.WebAuthnCredentials.Where(wa => targetIds.Contains(wa.UserId)).ExecuteDeleteAsync(); + await dbContext.Ciphers.Where(c => targetIds.Contains(c.UserId ?? default)).ExecuteDeleteAsync(); + await dbContext.Folders.Where(f => targetIds.Contains(f.UserId)).ExecuteDeleteAsync(); + await dbContext.AuthRequests.Where(a => targetIds.Contains(a.UserId)).ExecuteDeleteAsync(); + await dbContext.Devices.Where(d => targetIds.Contains(d.UserId)).ExecuteDeleteAsync(); + var collectionUsers = from cu in dbContext.CollectionUsers + join ou in dbContext.OrganizationUsers on cu.OrganizationUserId equals ou.Id + where targetIds.Contains(ou.UserId ?? default) + select cu; + dbContext.CollectionUsers.RemoveRange(collectionUsers); + var groupUsers = from gu in dbContext.GroupUsers + join ou in dbContext.OrganizationUsers on gu.OrganizationUserId equals ou.Id + where targetIds.Contains(ou.UserId ?? default) + select gu; + dbContext.GroupUsers.RemoveRange(groupUsers); + await dbContext.UserProjectAccessPolicy.Where(ap => targetIds.Contains(ap.OrganizationUser.UserId ?? default)).ExecuteDeleteAsync(); + await dbContext.UserServiceAccountAccessPolicy.Where(ap => targetIds.Contains(ap.OrganizationUser.UserId ?? default)).ExecuteDeleteAsync(); + await dbContext.OrganizationUsers.Where(ou => targetIds.Contains(ou.UserId ?? default)).ExecuteDeleteAsync(); + await dbContext.ProviderUsers.Where(pu => targetIds.Contains(pu.UserId ?? default)).ExecuteDeleteAsync(); + await dbContext.SsoUsers.Where(su => targetIds.Contains(su.UserId)).ExecuteDeleteAsync(); + await dbContext.EmergencyAccesses.Where(ea => targetIds.Contains(ea.GrantorId) || targetIds.Contains(ea.GranteeId ?? default)).ExecuteDeleteAsync(); + await dbContext.Sends.Where(s => targetIds.Contains(s.UserId ?? default)).ExecuteDeleteAsync(); + await dbContext.NotificationStatuses.Where(ns => targetIds.Contains(ns.UserId)).ExecuteDeleteAsync(); + await dbContext.Notifications.Where(n => targetIds.Contains(n.UserId ?? default)).ExecuteDeleteAsync(); + + foreach (var u in users) + { + var mappedUser = Mapper.Map(u); + dbContext.Users.Remove(mappedUser); + } + + await transaction.CommitAsync(); await dbContext.SaveChangesAsync(); } diff --git a/src/Sql/dbo/Stored Procedures/User_DeleteByIds.sql b/src/Sql/dbo/Stored Procedures/User_DeleteByIds.sql new file mode 100644 index 0000000000..97ab955f83 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/User_DeleteByIds.sql @@ -0,0 +1,158 @@ +CREATE PROCEDURE [dbo].[User_DeleteByIds] + @Ids NVARCHAR(MAX) +WITH RECOMPILE +AS +BEGIN + SET NOCOUNT ON + -- Declare a table variable to hold the parsed JSON data + DECLARE @ParsedIds TABLE (Id UNIQUEIDENTIFIER); + + -- Parse the JSON input into the table variable + INSERT INTO @ParsedIds (Id) + SELECT value + FROM OPENJSON(@Ids); + + -- Check if the input table is empty + IF (SELECT COUNT(1) FROM @ParsedIds) < 1 + BEGIN + RETURN(-1); + END + + DECLARE @BatchSize INT = 100 + + -- Delete ciphers + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION User_DeleteById_Ciphers + + DELETE TOP(@BatchSize) + FROM + [dbo].[Cipher] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION User_DeleteById_Ciphers + END + + BEGIN TRANSACTION User_DeleteById + + -- Delete WebAuthnCredentials + DELETE + FROM + [dbo].[WebAuthnCredential] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete folders + DELETE + FROM + [dbo].[Folder] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete AuthRequest, must be before Device + DELETE + FROM + [dbo].[AuthRequest] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete devices + DELETE + FROM + [dbo].[Device] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete collection users + DELETE + CU + FROM + [dbo].[CollectionUser] CU + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[Id] = CU.[OrganizationUserId] + WHERE + OU.[UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete group users + DELETE + GU + FROM + [dbo].[GroupUser] GU + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[Id] = GU.[OrganizationUserId] + WHERE + OU.[UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete AccessPolicy + DELETE + AP + FROM + [dbo].[AccessPolicy] AP + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[Id] = AP.[OrganizationUserId] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete organization users + DELETE + FROM + [dbo].[OrganizationUser] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete provider users + DELETE + FROM + [dbo].[ProviderUser] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete SSO Users + DELETE + FROM + [dbo].[SsoUser] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete Emergency Accesses + DELETE + FROM + [dbo].[EmergencyAccess] + WHERE + [GrantorId] IN (SELECT * FROM @ParsedIds) + OR + [GranteeId] IN (SELECT * FROM @ParsedIds) + + -- Delete Sends + DELETE + FROM + [dbo].[Send] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete Notification Status + DELETE + FROM + [dbo].[NotificationStatus] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete Notification + DELETE + FROM + [dbo].[Notification] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Finally, delete the user + DELETE + FROM + [dbo].[User] + WHERE + [Id] IN (SELECT * FROM @ParsedIds) + + COMMIT TRANSACTION User_DeleteById +END diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommandTests.cs index 81e83d7450..b21ae5459f 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommandTests.cs @@ -258,14 +258,15 @@ public class DeleteManagedOrganizationUserAccountCommandTests .Returns(new Dictionary { { orgUser1.Id, true }, { orgUser2.Id, true } }); // Act - var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, new[] { orgUser1.Id, orgUser2.Id }, null); + var userIds = new[] { orgUser1.Id, orgUser2.Id }; + var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, userIds, null); // Assert Assert.Equal(2, results.Count()); Assert.All(results, r => Assert.Empty(r.Item2)); - await sutProvider.GetDependency().Received(1).DeleteAsync(user1); - await sutProvider.GetDependency().Received(1).DeleteAsync(user2); + await sutProvider.GetDependency().Received(1).GetManyAsync(userIds); + await sutProvider.GetDependency().Received(1).DeleteManyAsync(Arg.Is>(users => users.Any(u => u.Id == user1.Id) && users.Any(u => u.Id == user2.Id))); await sutProvider.GetDependency().Received(1).LogOrganizationUserEventsAsync( Arg.Is>(events => events.Count(e => e.Item1.Id == orgUser1.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1 @@ -286,7 +287,9 @@ public class DeleteManagedOrganizationUserAccountCommandTests Assert.Single(result); Assert.Equal(orgUserId, result.First().Item1); Assert.Contains("Member not found.", result.First().Item2); - await sutProvider.GetDependency().Received(0).DeleteAsync(Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); await sutProvider.GetDependency().Received(0) .LogOrganizationUserEventsAsync(Arg.Any>()); } @@ -484,7 +487,6 @@ public class DeleteManagedOrganizationUserAccountCommandTests Assert.Equal("You cannot delete a member with Invited status.", results.First(r => r.Item1 == orgUser2.Id).Item2); Assert.Equal("Member is not managed by the organization.", results.First(r => r.Item1 == orgUser3.Id).Item2); - await sutProvider.GetDependency().Received(1).DeleteAsync(user1); await sutProvider.GetDependency().Received(1).LogOrganizationUserEventsAsync( Arg.Is>(events => events.Count(e => e.Item1.Id == orgUser1.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1)); diff --git a/test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs new file mode 100644 index 0000000000..d4606ae632 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs @@ -0,0 +1,99 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.Repositories; + +public class UserRepositoryTests +{ + [DatabaseTheory, DatabaseData] + public async Task DeleteAsync_Works(IUserRepository userRepository) + { + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@example.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + await userRepository.DeleteAsync(user); + + var deletedUser = await userRepository.GetByIdAsync(user.Id); + Assert.Null(deletedUser); + } + + [DatabaseTheory, DatabaseData] + public async Task DeleteManyAsync_Works(IUserRepository userRepository, IOrganizationUserRepository organizationUserRepository, IOrganizationRepository organizationRepository) + { + var user1 = await userRepository.CreateAsync(new User + { + Name = "Test User 1", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var user2 = await userRepository.CreateAsync(new User + { + Name = "Test User 2", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var user3 = await userRepository.CreateAsync(new User + { + Name = "Test User 3", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = user3.Email, // TODO: EF does not enfore this being NOT NULL + Plan = "Test", // TODO: EF does not enforce this being NOT NULl + }); + + await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user1.Id, + Status = OrganizationUserStatusType.Confirmed, + }); + + await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user3.Id, + Status = OrganizationUserStatusType.Confirmed, + }); + + await userRepository.DeleteManyAsync(new List + { + user1, + user2 + }); + + var deletedUser1 = await userRepository.GetByIdAsync(user1.Id); + var deletedUser2 = await userRepository.GetByIdAsync(user2.Id); + var notDeletedUser3 = await userRepository.GetByIdAsync(user3.Id); + + var orgUser1Deleted = await organizationUserRepository.GetByIdAsync(user1.Id); + + var notDeletedOrgUsers = await organizationUserRepository.GetManyByUserAsync(user3.Id); + + Assert.Null(deletedUser1); + Assert.Null(deletedUser2); + Assert.NotNull(notDeletedUser3); + + Assert.Null(orgUser1Deleted); + Assert.NotNull(notDeletedOrgUsers); + Assert.True(notDeletedOrgUsers.Count > 0); + } + +} diff --git a/util/Migrator/DbScripts/2024-11-22_00_UserDeleteByIds.sql b/util/Migrator/DbScripts/2024-11-22_00_UserDeleteByIds.sql new file mode 100644 index 0000000000..244151143e --- /dev/null +++ b/util/Migrator/DbScripts/2024-11-22_00_UserDeleteByIds.sql @@ -0,0 +1,158 @@ +CREATE OR ALTER PROCEDURE [dbo].[User_DeleteByIds] + @Ids NVARCHAR(MAX) +WITH RECOMPILE +AS +BEGIN + SET NOCOUNT ON + -- Declare a table variable to hold the parsed JSON data + DECLARE @ParsedIds TABLE (Id UNIQUEIDENTIFIER); + + -- Parse the JSON input into the table variable + INSERT INTO @ParsedIds (Id) + SELECT value + FROM OPENJSON(@Ids); + + -- Check if the input table is empty + IF (SELECT COUNT(1) FROM @ParsedIds) < 1 + BEGIN + RETURN(-1); + END + + DECLARE @BatchSize INT = 100 + + -- Delete ciphers + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION User_DeleteById_Ciphers + + DELETE TOP(@BatchSize) + FROM + [dbo].[Cipher] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION User_DeleteById_Ciphers + END + + BEGIN TRANSACTION User_DeleteById + + -- Delete WebAuthnCredentials + DELETE + FROM + [dbo].[WebAuthnCredential] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete folders + DELETE + FROM + [dbo].[Folder] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete AuthRequest, must be before Device + DELETE + FROM + [dbo].[AuthRequest] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete devices + DELETE + FROM + [dbo].[Device] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete collection users + DELETE + CU + FROM + [dbo].[CollectionUser] CU + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[Id] = CU.[OrganizationUserId] + WHERE + OU.[UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete group users + DELETE + GU + FROM + [dbo].[GroupUser] GU + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[Id] = GU.[OrganizationUserId] + WHERE + OU.[UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete AccessPolicy + DELETE + AP + FROM + [dbo].[AccessPolicy] AP + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[Id] = AP.[OrganizationUserId] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete organization users + DELETE + FROM + [dbo].[OrganizationUser] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete provider users + DELETE + FROM + [dbo].[ProviderUser] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete SSO Users + DELETE + FROM + [dbo].[SsoUser] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete Emergency Accesses + DELETE + FROM + [dbo].[EmergencyAccess] + WHERE + [GrantorId] IN (SELECT * FROM @ParsedIds) + OR + [GranteeId] IN (SELECT * FROM @ParsedIds) + + -- Delete Sends + DELETE + FROM + [dbo].[Send] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete Notification Status + DELETE + FROM + [dbo].[NotificationStatus] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete Notification + DELETE + FROM + [dbo].[Notification] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Finally, delete the user + DELETE + FROM + [dbo].[User] + WHERE + [Id] IN (SELECT * FROM @ParsedIds) + + COMMIT TRANSACTION User_DeleteById +END From 2212f552aa1ecc9d643592889ee408c0718270c4 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:56:12 -0500 Subject: [PATCH 45/94] Updated quartz jobs to create a container scope to allow for scoped services (#5131) --- src/Core/Jobs/JobFactory.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Core/Jobs/JobFactory.cs b/src/Core/Jobs/JobFactory.cs index ee95c6b2d6..6529443d97 100644 --- a/src/Core/Jobs/JobFactory.cs +++ b/src/Core/Jobs/JobFactory.cs @@ -1,4 +1,5 @@ -using Quartz; +using Microsoft.Extensions.DependencyInjection; +using Quartz; using Quartz.Spi; namespace Bit.Core.Jobs; @@ -14,7 +15,8 @@ public class JobFactory : IJobFactory public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) { - return _container.GetService(bundle.JobDetail.JobType) as IJob; + var scope = _container.CreateScope(); + return scope.ServiceProvider.GetService(bundle.JobDetail.JobType) as IJob; } public void ReturnJob(IJob job) From 127f1fd34d65c1ab5a60f0cd8c7d09e8d8021d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:14:34 +0000 Subject: [PATCH 46/94] =?UTF-8?q?[PM-10338]=C2=A0Update=20the=20Organizati?= =?UTF-8?q?on=20'Leave'=20endpoint=20to=20log=20EventType.OrganizationUser?= =?UTF-8?q?=5FLeft=20(#4908)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement UserLeaveAsync in IRemoveOrganizationUserCommand and refactor OrganizationsController to use it * Edit summary message for IRemoveOrganizationUserCommand.UserLeaveAsync * Refactor RemoveOrganizationUserCommand.RemoveUsersAsync to log in bulk --------- Co-authored-by: Matt Bishop --- .../Controllers/OrganizationsController.cs | 2 +- .../IRemoveOrganizationUserCommand.cs | 7 + .../RemoveOrganizationUserCommand.cs | 12 +- .../OrganizationsControllerTests.cs | 4 +- .../RemoveOrganizationUserCommandTests.cs | 136 +++++++++++++++++- 5 files changed, 153 insertions(+), 8 deletions(-) diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 4421af3a9a..226fe83c73 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -259,7 +259,7 @@ public class OrganizationsController : Controller throw new BadRequestException("Managed user account cannot leave managing organization. Contact your organization administrator for additional details."); } - await _removeOrganizationUserCommand.RemoveUserAsync(id, user.Id); + await _removeOrganizationUserCommand.UserLeaveAsync(id, user.Id); } [HttpDelete("{id}")] diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs index 7c1cdf05f8..605a5f5aee 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs @@ -50,4 +50,11 @@ public interface IRemoveOrganizationUserCommand /// Task> RemoveUsersAsync( Guid organizationId, IEnumerable organizationUserIds, EventSystemUser eventSystemUser); + + /// + /// Removes a user from an organization when they have left voluntarily. This should only be called by the same user who is being removed. + /// + /// Organization to leave. + /// User to leave. + Task UserLeaveAsync(Guid organizationId, Guid userId); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs index fa027f8e47..e45f109df1 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs @@ -114,6 +114,16 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand return result.Select(r => (r.OrganizationUser.Id, r.ErrorMessage)); } + public async Task UserLeaveAsync(Guid organizationId, Guid userId) + { + var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId); + ValidateRemoveUser(organizationId, organizationUser); + + await RepositoryRemoveUserAsync(organizationUser, deletingUserId: null, eventSystemUser: null); + + await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Left); + } + private void ValidateRemoveUser(Guid organizationId, OrganizationUser orgUser) { if (orgUser == null || orgUser.OrganizationId != organizationId) @@ -234,7 +244,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand await _organizationUserRepository.DeleteManyAsync(organizationUsersToRemove.Select(ou => ou.Id)); foreach (var orgUser in organizationUsersToRemove.Where(ou => ou.UserId.HasValue)) { - await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value); + await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId!.Value); } } diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index e58cd05b9d..f35dbaa5cf 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -130,7 +130,7 @@ public class OrganizationsControllerTests : IDisposable Assert.Contains("Your organization's Single Sign-On settings prevent you from leaving.", exception.Message); - await _removeOrganizationUserCommand.DidNotReceiveWithAnyArgs().RemoveUserAsync(default, default); + await _removeOrganizationUserCommand.DidNotReceiveWithAnyArgs().UserLeaveAsync(default, default); } [Theory, AutoData] @@ -193,7 +193,7 @@ public class OrganizationsControllerTests : IDisposable await _sut.Leave(orgId); - await _removeOrganizationUserCommand.Received(1).RemoveUserAsync(orgId, user.Id); + await _removeOrganizationUserCommand.Received(1).UserLeaveAsync(orgId, user.Id); } [Theory, AutoData] diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs index 61371b756e..6ab8236b8e 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs @@ -202,6 +202,14 @@ public class RemoveOrganizationUserCommandTests .HasConfirmedOwnersExceptAsync( organizationUser.OrganizationId, Arg.Is>(i => i.Contains(organizationUser.Id)), true); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); } [Theory, BitAutoData] @@ -346,9 +354,7 @@ public class RemoveOrganizationUserCommandTests [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, SutProvider sutProvider) { - var organizationUserRepository = sutProvider.GetDependency(); - - organizationUserRepository + sutProvider.GetDependency() .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value) .Returns(organizationUser); @@ -361,7 +367,13 @@ public class RemoveOrganizationUserCommandTests await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value); - await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(organizationUser); + + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); } [Theory, BitAutoData] @@ -370,6 +382,14 @@ public class RemoveOrganizationUserCommandTests { // Act & Assert await Assert.ThrowsAsync(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, userId)); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); } [Theory, BitAutoData] @@ -413,6 +433,14 @@ public class RemoveOrganizationUserCommandTests organizationUser.OrganizationId, Arg.Is>(i => i.Contains(organizationUser.Id)), Arg.Any()); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); } [Theory, BitAutoData] @@ -424,6 +452,7 @@ public class RemoveOrganizationUserCommandTests var sutProvider = SutProviderFactory(); var eventDate = sutProvider.GetDependency().GetUtcNow().UtcDateTime; orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; + var organizationUsers = new[] { orgUser1, orgUser2 }; var organizationUserIds = organizationUsers.Select(u => u.Id); @@ -774,6 +803,105 @@ public class RemoveOrganizationUserCommandTests Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message); } + [Theory, BitAutoData] + public async Task UserLeave_Success( + [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value) + .Returns(organizationUser); + + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), + Arg.Any()) + .Returns(true); + + await sutProvider.Sut.UserLeaveAsync(organizationUser.OrganizationId, organizationUser.UserId.Value); + + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(organizationUser); + + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Left); + } + + [Theory, BitAutoData] + public async Task UserLeave_NotFound_ThrowsException(SutProvider sutProvider, + Guid organizationId, Guid userId) + { + await Assert.ThrowsAsync(async () => await sutProvider.Sut.UserLeaveAsync(organizationId, userId)); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); + } + + [Theory, BitAutoData] + public async Task UserLeave_InvalidUser_ThrowsException(OrganizationUser organizationUser, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value) + .Returns(organizationUser); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UserLeaveAsync(Guid.NewGuid(), organizationUser.UserId.Value)); + + Assert.Contains(RemoveOrganizationUserCommand.UserNotFoundErrorMessage, exception.Message); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); + } + + [Theory, BitAutoData] + public async Task UserLeave_RemovingLastOwner_ThrowsException( + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value) + .Returns(organizationUser); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), + Arg.Any()) + .Returns(false); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UserLeaveAsync(organizationUser.OrganizationId, organizationUser.UserId.Value)); + + Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message); + _ = sutProvider.GetDependency() + .Received(1) + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), + Arg.Any()); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync((OrganizationUser)default, default); + } + /// /// Returns a new SutProvider with a FakeTimeProvider registered in the Sut. /// From 9e860104f209277e7c38a3e38e7c6e6eea6bb507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ch=C4=99ci=C5=84ski?= Date: Tue, 10 Dec 2024 15:30:34 +0100 Subject: [PATCH 47/94] BRE-311 Fix the MsSqlMigratorUtility failing silently (#5134) --- util/MsSqlMigratorUtility/Program.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/util/MsSqlMigratorUtility/Program.cs b/util/MsSqlMigratorUtility/Program.cs index 056cb696f8..0a65617eb7 100644 --- a/util/MsSqlMigratorUtility/Program.cs +++ b/util/MsSqlMigratorUtility/Program.cs @@ -9,7 +9,7 @@ internal class Program } [DefaultCommand] - public void Execute( + public int Execute( [Operand(Description = "Database connection string")] string databaseConnectionString, [Option('r', "repeatable", Description = "Mark scripts as repeatable")] @@ -20,7 +20,11 @@ internal class Program bool dryRun = false, [Option("no-transaction", Description = "Run without adding transaction per script or all scripts")] bool noTransactionMigration = false - ) => MigrateDatabase(databaseConnectionString, repeatable, folderName, dryRun, noTransactionMigration); + ) + { + return MigrateDatabase(databaseConnectionString, repeatable, folderName, dryRun, noTransactionMigration) ? 0 : -1; + } + private static bool MigrateDatabase(string databaseConnectionString, bool repeatable = false, string folderName = "", bool dryRun = false, bool noTransactionMigration = false) From 9c8f932149f647d0a6153f82417b6effb0e36fca Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Tue, 10 Dec 2024 09:55:03 -0500 Subject: [PATCH 48/94] [PM-12273] Integration page (#5119) * add feature flag * add rest endpoint to get plan type for organization --- .../Controllers/OrganizationsController.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 226fe83c73..4e01bb3451 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -540,4 +540,17 @@ public class OrganizationsController : Controller await _organizationService.UpdateAsync(model.ToOrganization(organization, _featureService), eventType: EventType.Organization_CollectionManagement_Updated); return new OrganizationResponseModel(organization); } + + [HttpGet("{id}/plan-type")] + public async Task GetPlanType(string id) + { + var orgIdGuid = new Guid(id); + var organization = await _organizationRepository.GetByIdAsync(orgIdGuid); + if (organization == null) + { + throw new NotFoundException(); + } + + return organization.PlanType; + } } From 4730d2dab7c9f43f162bf90e689c9c24c7595e91 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Tue, 10 Dec 2024 09:55:36 -0500 Subject: [PATCH 49/94] add feature flag (#5114) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 75e154f041..c4027e1689 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -158,6 +158,7 @@ public static class FeatureFlagKeys public const string MacOsNativeCredentialSync = "macos-native-credential-sync"; public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; public const string InlineMenuTotp = "inline-menu-totp"; + public const string PM12443RemovePagingLogic = "pm-12443-remove-paging-logic"; public const string SelfHostLicenseRefactor = "pm-11516-self-host-license-refactor"; public static List GetAllKeys() From fe70db3e878afea124fb84b0cd8b9b0cc9867a77 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Tue, 10 Dec 2024 16:42:14 +0100 Subject: [PATCH 50/94] [PM-12765] Display error when attempting to autoscale canceled subscription (#5132) --- .../Services/Implementations/OrganizationService.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index eebe76baef..49f339cc9a 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -14,6 +14,7 @@ using Bit.Core.Auth.Models.Business; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Services; @@ -1324,6 +1325,12 @@ public class OrganizationService : IOrganizationService return (false, $"Seat limit has been reached."); } + var subscription = await _paymentService.GetSubscriptionAsync(organization); + if (subscription?.Subscription?.Status == StripeConstants.SubscriptionStatus.Canceled) + { + return (false, "Cannot autoscale with a canceled subscription."); + } + return (true, failureReason); } From 2d257dc27434dd201d211a14a82f3fed112a3438 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Tue, 10 Dec 2024 12:29:54 -0500 Subject: [PATCH 51/94] chore: run `dotnet format` (#5137) --- util/MsSqlMigratorUtility/Program.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/util/MsSqlMigratorUtility/Program.cs b/util/MsSqlMigratorUtility/Program.cs index 0a65617eb7..c9f984b6de 100644 --- a/util/MsSqlMigratorUtility/Program.cs +++ b/util/MsSqlMigratorUtility/Program.cs @@ -21,9 +21,9 @@ internal class Program [Option("no-transaction", Description = "Run without adding transaction per script or all scripts")] bool noTransactionMigration = false ) - { - return MigrateDatabase(databaseConnectionString, repeatable, folderName, dryRun, noTransactionMigration) ? 0 : -1; - } + { + return MigrateDatabase(databaseConnectionString, repeatable, folderName, dryRun, noTransactionMigration) ? 0 : -1; + } private static bool MigrateDatabase(string databaseConnectionString, From 39ce7637c99ba2719395216e14044719001b111b Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:50:06 -0600 Subject: [PATCH 52/94] fix: remove policy definitions feature flag, refs PM-14245 (#5139) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index c4027e1689..9f326bdb19 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -144,7 +144,6 @@ public static class FeatureFlagKeys public const string AccessIntelligence = "pm-13227-access-intelligence"; public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises"; - public const string Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions"; public const string GeneratorToolsModernization = "generator-tools-modernization"; public const string NewDeviceVerification = "new-device-verification"; public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; From 94761a8c7b6c6d8ec94aaf196762b62078a01d17 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:21:26 -0500 Subject: [PATCH 53/94] [deps] Billing: Update FluentAssertions to v7 (#5127) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- test/Billing.Test/Billing.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Billing.Test/Billing.Test.csproj b/test/Billing.Test/Billing.Test.csproj index c6a7ef48e0..4d71425681 100644 --- a/test/Billing.Test/Billing.Test.csproj +++ b/test/Billing.Test/Billing.Test.csproj @@ -6,7 +6,7 @@ - + From 674e5228433d0fa493c1e2f8abce80a83f0ce4a1 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Wed, 11 Dec 2024 10:32:28 +0100 Subject: [PATCH 54/94] =?UTF-8?q?[PM-6201]=20Self-Host=20Admin=20Portal=20?= =?UTF-8?q?is=20reporting=20"10239=20GB=20of=20Additional=E2=80=A6=20(#513?= =?UTF-8?q?0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminConsole/Views/Organizations/Index.cshtml | 12 ++---------- src/Core/AdminConsole/Entities/Organization.cs | 5 +++++ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Admin/AdminConsole/Views/Organizations/Index.cshtml b/src/Admin/AdminConsole/Views/Organizations/Index.cshtml index 756cd76f62..3dc6801490 100644 --- a/src/Admin/AdminConsole/Views/Organizations/Index.cshtml +++ b/src/Admin/AdminConsole/Views/Organizations/Index.cshtml @@ -81,16 +81,8 @@ } } - @if(org.MaxStorageGb.HasValue && org.MaxStorageGb > 1) - { - - } - else - { - - } + @if(org.Enabled) { , IStorableSubscriber, IRevisable, public bool IsExpired() => ExpirationDate.HasValue && ExpirationDate.Value <= DateTime.UtcNow; + /// + /// Used storage in gigabytes. + /// + public double StorageGb => Storage.HasValue ? Math.Round(Storage.Value / 1073741824D, 2) : 0; + public long StorageBytesRemaining() { if (!MaxStorageGb.HasValue) From 9b478107b67bb863632af87f4bffc4a0772e7e16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:09:12 +0000 Subject: [PATCH 55/94] [PM-15128] Add Promote Provider Service User functionality to Bitwarden Portal (#5118) * Add Promote Provider Service User feature to Admin Portal * Rename feature flag key for Promote Provider Service User tool --- src/Admin/Controllers/ToolsController.cs | 45 +++++++++++++++++++ src/Admin/Enums/Permissions.cs | 1 + .../Models/PromoteProviderServiceUserModel.cs | 13 ++++++ src/Admin/Utilities/RolePermissionMapping.cs | 2 + src/Admin/Views/Shared/_Layout.cshtml | 12 ++++- .../Tools/PromoteProviderServiceUser.cshtml | 25 +++++++++++ src/Core/Constants.cs | 1 + 7 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/Admin/Models/PromoteProviderServiceUserModel.cs create mode 100644 src/Admin/Views/Tools/PromoteProviderServiceUser.cshtml diff --git a/src/Admin/Controllers/ToolsController.cs b/src/Admin/Controllers/ToolsController.cs index 3e092b90af..ea91d01cb8 100644 --- a/src/Admin/Controllers/ToolsController.cs +++ b/src/Admin/Controllers/ToolsController.cs @@ -3,7 +3,9 @@ using System.Text.Json; using Bit.Admin.Enums; using Bit.Admin.Models; using Bit.Admin.Utilities; +using Bit.Core; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.Entities; using Bit.Core.Models.BitStripe; using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; @@ -28,6 +30,7 @@ public class ToolsController : Controller private readonly ITransactionRepository _transactionRepository; private readonly IInstallationRepository _installationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IProviderUserRepository _providerUserRepository; private readonly IPaymentService _paymentService; private readonly ITaxRateRepository _taxRateRepository; private readonly IStripeAdapter _stripeAdapter; @@ -41,6 +44,7 @@ public class ToolsController : Controller ITransactionRepository transactionRepository, IInstallationRepository installationRepository, IOrganizationUserRepository organizationUserRepository, + IProviderUserRepository providerUserRepository, ITaxRateRepository taxRateRepository, IPaymentService paymentService, IStripeAdapter stripeAdapter, @@ -53,6 +57,7 @@ public class ToolsController : Controller _transactionRepository = transactionRepository; _installationRepository = installationRepository; _organizationUserRepository = organizationUserRepository; + _providerUserRepository = providerUserRepository; _taxRateRepository = taxRateRepository; _paymentService = paymentService; _stripeAdapter = stripeAdapter; @@ -220,6 +225,46 @@ public class ToolsController : Controller return RedirectToAction("Edit", "Organizations", new { id = model.OrganizationId.Value }); } + [RequireFeature(FeatureFlagKeys.PromoteProviderServiceUserTool)] + [RequirePermission(Permission.Tools_PromoteProviderServiceUser)] + public IActionResult PromoteProviderServiceUser() + { + return View(); + } + + [HttpPost] + [ValidateAntiForgeryToken] + [RequireFeature(FeatureFlagKeys.PromoteProviderServiceUserTool)] + [RequirePermission(Permission.Tools_PromoteProviderServiceUser)] + public async Task PromoteProviderServiceUser(PromoteProviderServiceUserModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var providerUsers = await _providerUserRepository.GetManyByProviderAsync( + model.ProviderId.Value, null); + var serviceUser = providerUsers.FirstOrDefault(u => u.UserId == model.UserId.Value); + if (serviceUser == null) + { + ModelState.AddModelError(nameof(model.UserId), "Service User Id not found in this provider."); + } + else if (serviceUser.Type != Core.AdminConsole.Enums.Provider.ProviderUserType.ServiceUser) + { + ModelState.AddModelError(nameof(model.UserId), "User is not a service user of this provider."); + } + + if (!ModelState.IsValid) + { + return View(model); + } + + serviceUser.Type = Core.AdminConsole.Enums.Provider.ProviderUserType.ProviderAdmin; + await _providerUserRepository.ReplaceAsync(serviceUser); + return RedirectToAction("Edit", "Providers", new { id = model.ProviderId.Value }); + } + [RequirePermission(Permission.Tools_GenerateLicenseFile)] public IActionResult GenerateLicense() { diff --git a/src/Admin/Enums/Permissions.cs b/src/Admin/Enums/Permissions.cs index 274db11cb4..c878267f89 100644 --- a/src/Admin/Enums/Permissions.cs +++ b/src/Admin/Enums/Permissions.cs @@ -44,6 +44,7 @@ public enum Permission Tools_ChargeBrainTreeCustomer, Tools_PromoteAdmin, + Tools_PromoteProviderServiceUser, Tools_GenerateLicenseFile, Tools_ManageTaxRates, Tools_ManageStripeSubscriptions, diff --git a/src/Admin/Models/PromoteProviderServiceUserModel.cs b/src/Admin/Models/PromoteProviderServiceUserModel.cs new file mode 100644 index 0000000000..5d6ca03ce6 --- /dev/null +++ b/src/Admin/Models/PromoteProviderServiceUserModel.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Admin.Models; + +public class PromoteProviderServiceUserModel +{ + [Required] + [Display(Name = "Provider Service User Id")] + public Guid? UserId { get; set; } + [Required] + [Display(Name = "Provider Id")] + public Guid? ProviderId { get; set; } +} diff --git a/src/Admin/Utilities/RolePermissionMapping.cs b/src/Admin/Utilities/RolePermissionMapping.cs index e260c264f4..81da3fcf38 100644 --- a/src/Admin/Utilities/RolePermissionMapping.cs +++ b/src/Admin/Utilities/RolePermissionMapping.cs @@ -45,6 +45,7 @@ public static class RolePermissionMapping Permission.Provider_ResendEmailInvite, Permission.Tools_ChargeBrainTreeCustomer, Permission.Tools_PromoteAdmin, + Permission.Tools_PromoteProviderServiceUser, Permission.Tools_GenerateLicenseFile, Permission.Tools_ManageTaxRates, Permission.Tools_ManageStripeSubscriptions @@ -91,6 +92,7 @@ public static class RolePermissionMapping Permission.Provider_ResendEmailInvite, Permission.Tools_ChargeBrainTreeCustomer, Permission.Tools_PromoteAdmin, + Permission.Tools_PromoteProviderServiceUser, Permission.Tools_GenerateLicenseFile, Permission.Tools_ManageTaxRates, Permission.Tools_ManageStripeSubscriptions, diff --git a/src/Admin/Views/Shared/_Layout.cshtml b/src/Admin/Views/Shared/_Layout.cshtml index 62cc5706df..b1f0a24420 100644 --- a/src/Admin/Views/Shared/_Layout.cshtml +++ b/src/Admin/Views/Shared/_Layout.cshtml @@ -1,8 +1,10 @@ @using Bit.Admin.Enums; +@using Bit.Core @inject SignInManager SignInManager @inject Bit.Core.Settings.GlobalSettings GlobalSettings @inject Bit.Admin.Services.IAccessControlService AccessControlService +@inject Bit.Core.Services.IFeatureService FeatureService @{ var canViewUsers = AccessControlService.UserHasPermission(Permission.User_List_View); @@ -11,13 +13,15 @@ var canChargeBraintree = AccessControlService.UserHasPermission(Permission.Tools_ChargeBrainTreeCustomer); var canCreateTransaction = AccessControlService.UserHasPermission(Permission.Tools_CreateEditTransaction); var canPromoteAdmin = AccessControlService.UserHasPermission(Permission.Tools_PromoteAdmin); + var canPromoteProviderServiceUser = FeatureService.IsEnabled(FeatureFlagKeys.PromoteProviderServiceUserTool) && + AccessControlService.UserHasPermission(Permission.Tools_PromoteProviderServiceUser); var canGenerateLicense = AccessControlService.UserHasPermission(Permission.Tools_GenerateLicenseFile); var canManageTaxRates = AccessControlService.UserHasPermission(Permission.Tools_ManageTaxRates); var canManageStripeSubscriptions = AccessControlService.UserHasPermission(Permission.Tools_ManageStripeSubscriptions); var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents); var canMigrateProviders = AccessControlService.UserHasPermission(Permission.Tools_MigrateProviders); - var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin || + var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin || canPromoteProviderServiceUser || canGenerateLicense || canManageTaxRates || canManageStripeSubscriptions; } @@ -91,6 +95,12 @@ Promote Admin } + @if (canPromoteProviderServiceUser) + { + + Promote Provider Service User + + } @if (canGenerateLicense) { diff --git a/src/Admin/Views/Tools/PromoteProviderServiceUser.cshtml b/src/Admin/Views/Tools/PromoteProviderServiceUser.cshtml new file mode 100644 index 0000000000..7ff45fce53 --- /dev/null +++ b/src/Admin/Views/Tools/PromoteProviderServiceUser.cshtml @@ -0,0 +1,25 @@ +@model PromoteProviderServiceUserModel +@{ + ViewData["Title"] = "Promote Provider Service User"; +} + +

Promote Provider Service User

+ +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
\ No newline at end of file diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 9f326bdb19..cf7feecc85 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -159,6 +159,7 @@ public static class FeatureFlagKeys public const string InlineMenuTotp = "inline-menu-totp"; public const string PM12443RemovePagingLogic = "pm-12443-remove-paging-logic"; public const string SelfHostLicenseRefactor = "pm-11516-self-host-license-refactor"; + public const string PromoteProviderServiceUserTool = "pm-15128-promote-provider-service-user-tool"; public static List GetAllKeys() { From 09db6c79cbce367ac72debbcf55fdfe86a9800dc Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Wed, 11 Dec 2024 06:31:22 -0500 Subject: [PATCH 56/94] chore(codeowners): assign a bunch of workflows to platform (#5136) --- .github/CODEOWNERS | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1d142016f2..9784e1f9ab 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,11 +15,7 @@ ## These are shared workflows ## .github/workflows/_move_finalization_db_scripts.yml -.github/workflows/build.yml -.github/workflows/cleanup-after-pr.yml -.github/workflows/cleanup-rc-branch.yml .github/workflows/release.yml -.github/workflows/repository-management.yml # Database Operations for database changes src/Sql/** @bitwarden/dept-dbops @@ -68,6 +64,14 @@ src/EventsProcessor @bitwarden/team-admin-console-dev src/Admin/Controllers/ToolsController.cs @bitwarden/team-billing-dev src/Admin/Views/Tools @bitwarden/team-billing-dev +# Platform team +.github/workflows/build.yml @bitwarden/team-platform-dev +.github/workflows/cleanup-after-pr.yml @bitwarden/team-platform-dev +.github/workflows/cleanup-rc-branch.yml @bitwarden/team-platform-dev +.github/workflows/repository-management.yml @bitwarden/team-platform-dev +.github/workflows/test-database.yml @bitwarden/team-platform-dev +.github/workflows/test.yml @bitwarden/team-platform-dev + # Multiple owners - DO NOT REMOVE (BRE) **/packages.lock.json Directory.Build.props From 64573d01a38823424976633b3ff822cd8db03f9e Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Wed, 11 Dec 2024 14:56:46 +0100 Subject: [PATCH 57/94] [PM-6201] Fix creation of organizations no longer working after merging #5130 (#5142) --- src/Admin/AdminConsole/Models/OrganizationsModel.cs | 2 ++ src/Admin/AdminConsole/Views/Organizations/Index.cshtml | 3 +-- src/Core/AdminConsole/Entities/Organization.cs | 5 ----- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Admin/AdminConsole/Models/OrganizationsModel.cs b/src/Admin/AdminConsole/Models/OrganizationsModel.cs index 147c5275f8..a98985ef01 100644 --- a/src/Admin/AdminConsole/Models/OrganizationsModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationsModel.cs @@ -10,4 +10,6 @@ public class OrganizationsModel : PagedModel public bool? Paid { get; set; } public string Action { get; set; } public bool SelfHosted { get; set; } + + public double StorageGB(Organization org) => org.Storage.HasValue ? Math.Round(org.Storage.Value / 1073741824D, 2) : 0; } diff --git a/src/Admin/AdminConsole/Views/Organizations/Index.cshtml b/src/Admin/AdminConsole/Views/Organizations/Index.cshtml index 3dc6801490..d42d0e8aa2 100644 --- a/src/Admin/AdminConsole/Views/Organizations/Index.cshtml +++ b/src/Admin/AdminConsole/Views/Organizations/Index.cshtml @@ -81,8 +81,7 @@ } } - + @if(org.Enabled) { , IStorableSubscriber, IRevisable, public bool IsExpired() => ExpirationDate.HasValue && ExpirationDate.Value <= DateTime.UtcNow; - /// - /// Used storage in gigabytes. - /// - public double StorageGb => Storage.HasValue ? Math.Round(Storage.Value / 1073741824D, 2) : 0; - public long StorageBytesRemaining() { if (!MaxStorageGb.HasValue) From c99b4106f544920ec7496c1cc1e4dbeff6aa597e Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Wed, 11 Dec 2024 15:19:38 +0100 Subject: [PATCH 58/94] Revert [PM-6201] (#5143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "[PM-6201] Fix creation of organizations no longer working after merging #5130 (#5142)" This reverts commit 64573d01a38823424976633b3ff822cd8db03f9e. * Revert "[PM-6201] Self-Host Admin Portal is reporting "10239 GB of Additional… (#5130)" This reverts commit 674e5228433d0fa493c1e2f8abce80a83f0ce4a1. --- src/Admin/AdminConsole/Models/OrganizationsModel.cs | 2 -- .../AdminConsole/Views/Organizations/Index.cshtml | 11 ++++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Admin/AdminConsole/Models/OrganizationsModel.cs b/src/Admin/AdminConsole/Models/OrganizationsModel.cs index a98985ef01..147c5275f8 100644 --- a/src/Admin/AdminConsole/Models/OrganizationsModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationsModel.cs @@ -10,6 +10,4 @@ public class OrganizationsModel : PagedModel public bool? Paid { get; set; } public string Action { get; set; } public bool SelfHosted { get; set; } - - public double StorageGB(Organization org) => org.Storage.HasValue ? Math.Round(org.Storage.Value / 1073741824D, 2) : 0; } diff --git a/src/Admin/AdminConsole/Views/Organizations/Index.cshtml b/src/Admin/AdminConsole/Views/Organizations/Index.cshtml index d42d0e8aa2..756cd76f62 100644 --- a/src/Admin/AdminConsole/Views/Organizations/Index.cshtml +++ b/src/Admin/AdminConsole/Views/Organizations/Index.cshtml @@ -81,7 +81,16 @@ } } - + @if(org.MaxStorageGb.HasValue && org.MaxStorageGb > 1) + { + + } + else + { + + } @if(org.Enabled) { Date: Wed, 11 Dec 2024 14:48:00 +0000 Subject: [PATCH 59/94] Update unclaimed domains email copy (#5116) --- .../OrganizationDomainService.cs | 17 +++++++++--- .../OrganizationDomainUnclaimed.html.hbs | 27 +++++++++++++++++++ .../OrganizationDomainUnclaimed.text.hbs | 10 +++++++ src/Core/Services/IMailService.cs | 1 + .../Implementations/HandlebarsMailService.cs | 13 +++++++++ .../NoopImplementations/NoopMailService.cs | 5 ++++ 6 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 src/Core/MailTemplates/Handlebars/OrganizationDomainUnclaimed.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/OrganizationDomainUnclaimed.text.hbs diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs index 4ce33f3b5b..9b99cf71f0 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs @@ -17,6 +17,7 @@ public class OrganizationDomainService : IOrganizationDomainService private readonly TimeProvider _timeProvider; private readonly ILogger _logger; private readonly IGlobalSettings _globalSettings; + private readonly IFeatureService _featureService; public OrganizationDomainService( IOrganizationDomainRepository domainRepository, @@ -26,7 +27,8 @@ public class OrganizationDomainService : IOrganizationDomainService IVerifyOrganizationDomainCommand verifyOrganizationDomainCommand, TimeProvider timeProvider, ILogger logger, - IGlobalSettings globalSettings) + IGlobalSettings globalSettings, + IFeatureService featureService) { _domainRepository = domainRepository; _organizationUserRepository = organizationUserRepository; @@ -36,6 +38,7 @@ public class OrganizationDomainService : IOrganizationDomainService _timeProvider = timeProvider; _logger = logger; _globalSettings = globalSettings; + _featureService = featureService; } public async Task ValidateOrganizationsDomainAsync() @@ -90,8 +93,16 @@ public class OrganizationDomainService : IOrganizationDomainService //Send email to administrators if (adminEmails.Count > 0) { - await _mailService.SendUnverifiedOrganizationDomainEmailAsync(adminEmails, - domain.OrganizationId.ToString(), domain.DomainName); + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) + { + await _mailService.SendUnclaimedOrganizationDomainEmailAsync(adminEmails, + domain.OrganizationId.ToString(), domain.DomainName); + } + else + { + await _mailService.SendUnverifiedOrganizationDomainEmailAsync(adminEmails, + domain.OrganizationId.ToString(), domain.DomainName); + } } _logger.LogInformation(Constants.BypassFiltersEventId, "Expired domain: {domainName}", domain.DomainName); diff --git a/src/Core/MailTemplates/Handlebars/OrganizationDomainUnclaimed.html.hbs b/src/Core/MailTemplates/Handlebars/OrganizationDomainUnclaimed.html.hbs new file mode 100644 index 0000000000..cc42898c29 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/OrganizationDomainUnclaimed.html.hbs @@ -0,0 +1,27 @@ +{{#>FullHtmlLayout}} + + + + + + + + + + + + + +
+ The domain {{DomainName}} in your Bitwarden organization could not be claimed. +
+ Check the corresponding record in your domain host. Then reclaim this domain in Bitwarden to use it for your organization. +
+ The domain will be removed from your organization in 7 days if it is not claimed. +
+ + Manage Domains + +
+
+{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/OrganizationDomainUnclaimed.text.hbs b/src/Core/MailTemplates/Handlebars/OrganizationDomainUnclaimed.text.hbs new file mode 100644 index 0000000000..4f205c5054 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/OrganizationDomainUnclaimed.text.hbs @@ -0,0 +1,10 @@ +{{#>BasicTextLayout}} +The domain {{DomainName}} in your Bitwarden organization could not be claimed. + +Check the corresponding record in your domain host. Then reclaim this domain in Bitwarden to use it for your organization. + +The domain will be removed from your organization in 7 days if it is not claimed. + +{{Url}} + +{{/BasicTextLayout}} diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index c6c9dc7948..0f69d8daaf 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -85,6 +85,7 @@ public interface IMailService Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip); Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip); Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName); + Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName); Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable ownerEmails); Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable ownerEmails); Task SendTrustedDeviceAdminApprovalEmailAsync(string email, DateTime utcNow, string ip, string deviceTypeAndIdentifier); diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index c220df18a1..22341111f3 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -1068,6 +1068,19 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName) + { + var message = CreateDefaultMessage("Domain not claimed", adminEmails); + var model = new OrganizationDomainUnverifiedViewModel + { + Url = $"{_globalSettings.BaseServiceUri.VaultWithHash}/organizations/{organizationId}/settings/domain-verification", + DomainName = domainName + }; + await AddMessageContentAsync(message, "OrganizationDomainUnclaimed", model); + message.Category = "UnclaimedOrganizationDomain"; + await _mailDeliveryService.SendEmailAsync(message); + } + public async Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable ownerEmails) { diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index e8ea8d9863..4ce188c86b 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -273,6 +273,11 @@ public class NoopMailService : IMailService return Task.FromResult(0); } + public Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName) + { + return Task.FromResult(0); + } + public Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable ownerEmails) { From 9b732c739aa20654a0ecdb52a76927c75842cb67 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 11 Dec 2024 10:10:20 -0500 Subject: [PATCH 60/94] [PM-15907] Disable cipher key encryption on self-hosted instances (#5140) * Disable cipher key encryption on self-hosted instances * Removed override instead of setting to false --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index cf7feecc85..df0abfb4b9 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -175,7 +175,6 @@ public static class FeatureFlagKeys return new Dictionary() { { DuoRedirect, "true" }, - { CipherKeyEncryption, "true" }, }; } } From 4c502f8cc81bf543caafb3c5e1cb7e4849d09e78 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Wed, 11 Dec 2024 18:07:57 +0000 Subject: [PATCH 61/94] Bumped version to 2024.12.1 --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 8639ac4a0d..05598bbbe1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2024.12.0 + 2024.12.1 Bit.$(MSBuildProjectName) enable @@ -64,4 +64,4 @@
- + \ No newline at end of file From 2d891b396ac6248787969be75485b2262b46e8d2 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:55:00 -0500 Subject: [PATCH 62/94] [PM-11127] Write `OrganizationInstallation` record when license is retrieved (#5090) * Add SQL files * Add SQL Server migration * Add Core entity * Add Dapper repository * Add EF repository * Add EF migrations * Save OrganizationInstallation during GetLicense invocation * Run dotnet format --- .../Controllers/OrganizationsController.cs | 27 +- .../Entities/OrganizationInstallation.cs | 24 + .../IOrganizationInstallationRepository.cs | 10 + .../OrganizationInstallationRepository.cs | 39 + .../DapperServiceCollectionExtensions.cs | 1 + ...tionInstallationEntityTypeConfiguration.cs | 29 + .../Models/OrganizationInstallation.cs | 19 + .../OrganizationInstallationRepository.cs | 45 + .../Repositories/DatabaseContext.cs | 1 + .../OrganizationInstallation_Create.sql | 27 + .../OrganizationInstallation_DeleteById.sql | 12 + .../OrganizationInstallation_ReadById.sql | 13 + ...ationInstallation_ReadByInstallationId.sql | 13 + ...ationInstallation_ReadByOrganizationId.sql | 13 + .../OrganizationInstallation_Update.sql | 17 + .../dbo/Tables/OrganizationInstallation.sql | 18 + .../Views/OrganizationInstallationView.sql | 6 + .../OrganizationsControllerTests.cs | 6 +- ...6_00_AddTable_OrganizationInstallation.sql | 158 + ...Table_OrganizationInstallation.Designer.cs | 2988 ++++++++++++++++ ...85456_AddTable_OrganizationInstallation.cs | 58 + .../DatabaseContextModelSnapshot.cs | 48 + ...Table_OrganizationInstallation.Designer.cs | 2994 +++++++++++++++++ ...85450_AddTable_OrganizationInstallation.cs | 57 + .../DatabaseContextModelSnapshot.cs | 48 + ...Table_OrganizationInstallation.Designer.cs | 2977 ++++++++++++++++ ...85500_AddTable_OrganizationInstallation.cs | 57 + .../DatabaseContextModelSnapshot.cs | 48 + 28 files changed, 9751 insertions(+), 2 deletions(-) create mode 100644 src/Core/Billing/Entities/OrganizationInstallation.cs create mode 100644 src/Core/Billing/Repositories/IOrganizationInstallationRepository.cs create mode 100644 src/Infrastructure.Dapper/Billing/Repositories/OrganizationInstallationRepository.cs create mode 100644 src/Infrastructure.EntityFramework/Billing/Configurations/OrganizationInstallationEntityTypeConfiguration.cs create mode 100644 src/Infrastructure.EntityFramework/Billing/Models/OrganizationInstallation.cs create mode 100644 src/Infrastructure.EntityFramework/Billing/Repositories/OrganizationInstallationRepository.cs create mode 100644 src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_Create.sql create mode 100644 src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_DeleteById.sql create mode 100644 src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadById.sql create mode 100644 src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadByInstallationId.sql create mode 100644 src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadByOrganizationId.sql create mode 100644 src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_Update.sql create mode 100644 src/Sql/Billing/dbo/Tables/OrganizationInstallation.sql create mode 100644 src/Sql/Billing/dbo/Views/OrganizationInstallationView.sql create mode 100644 util/Migrator/DbScripts/2024-11-26_00_AddTable_OrganizationInstallation.sql create mode 100644 util/MySqlMigrations/Migrations/20241126185456_AddTable_OrganizationInstallation.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20241126185456_AddTable_OrganizationInstallation.cs create mode 100644 util/PostgresMigrations/Migrations/20241126185450_AddTable_OrganizationInstallation.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20241126185450_AddTable_OrganizationInstallation.cs create mode 100644 util/SqliteMigrations/Migrations/20241126185500_AddTable_OrganizationInstallation.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20241126185500_AddTable_OrganizationInstallation.cs diff --git a/src/Api/Billing/Controllers/OrganizationsController.cs b/src/Api/Billing/Controllers/OrganizationsController.cs index 75ae2fb89c..ccb30c6a77 100644 --- a/src/Api/Billing/Controllers/OrganizationsController.cs +++ b/src/Api/Billing/Controllers/OrganizationsController.cs @@ -6,7 +6,9 @@ using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Entities; using Bit.Core.Billing.Models; +using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Enums; @@ -42,7 +44,8 @@ public class OrganizationsController( IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand, IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand, IReferenceEventService referenceEventService, - ISubscriberService subscriberService) + ISubscriberService subscriberService, + IOrganizationInstallationRepository organizationInstallationRepository) : Controller { [HttpGet("{id:guid}/subscription")] @@ -97,6 +100,8 @@ public class OrganizationsController( throw new NotFoundException(); } + await SaveOrganizationInstallationAsync(id, installationId); + return license; } @@ -366,4 +371,24 @@ public class OrganizationsController( return await organizationRepository.GetByIdAsync(id); } + + private async Task SaveOrganizationInstallationAsync(Guid organizationId, Guid installationId) + { + var organizationInstallation = + await organizationInstallationRepository.GetByInstallationIdAsync(installationId); + + if (organizationInstallation == null) + { + await organizationInstallationRepository.CreateAsync(new OrganizationInstallation + { + OrganizationId = organizationId, + InstallationId = installationId + }); + } + else if (organizationInstallation.OrganizationId == organizationId) + { + organizationInstallation.RevisionDate = DateTime.UtcNow; + await organizationInstallationRepository.ReplaceAsync(organizationInstallation); + } + } } diff --git a/src/Core/Billing/Entities/OrganizationInstallation.cs b/src/Core/Billing/Entities/OrganizationInstallation.cs new file mode 100644 index 0000000000..4332afd44a --- /dev/null +++ b/src/Core/Billing/Entities/OrganizationInstallation.cs @@ -0,0 +1,24 @@ +using Bit.Core.Entities; +using Bit.Core.Utilities; + +namespace Bit.Core.Billing.Entities; + +#nullable enable + +public class OrganizationInstallation : ITableObject +{ + public Guid Id { get; set; } + + public Guid OrganizationId { get; set; } + public Guid InstallationId { get; set; } + public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; + public DateTime? RevisionDate { get; set; } + + public void SetNewId() + { + if (Id == default) + { + Id = CoreHelpers.GenerateComb(); + } + } +} diff --git a/src/Core/Billing/Repositories/IOrganizationInstallationRepository.cs b/src/Core/Billing/Repositories/IOrganizationInstallationRepository.cs new file mode 100644 index 0000000000..05710d3966 --- /dev/null +++ b/src/Core/Billing/Repositories/IOrganizationInstallationRepository.cs @@ -0,0 +1,10 @@ +using Bit.Core.Billing.Entities; +using Bit.Core.Repositories; + +namespace Bit.Core.Billing.Repositories; + +public interface IOrganizationInstallationRepository : IRepository +{ + Task GetByInstallationIdAsync(Guid installationId); + Task> GetByOrganizationIdAsync(Guid organizationId); +} diff --git a/src/Infrastructure.Dapper/Billing/Repositories/OrganizationInstallationRepository.cs b/src/Infrastructure.Dapper/Billing/Repositories/OrganizationInstallationRepository.cs new file mode 100644 index 0000000000..f73eefb793 --- /dev/null +++ b/src/Infrastructure.Dapper/Billing/Repositories/OrganizationInstallationRepository.cs @@ -0,0 +1,39 @@ +using System.Data; +using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Repositories; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; + +namespace Bit.Infrastructure.Dapper.Billing.Repositories; + +public class OrganizationInstallationRepository( + GlobalSettings globalSettings) : Repository( + globalSettings.SqlServer.ConnectionString, + globalSettings.SqlServer.ReadOnlyConnectionString), IOrganizationInstallationRepository +{ + public async Task GetByInstallationIdAsync(Guid installationId) + { + var sqlConnection = new SqlConnection(ConnectionString); + + var results = await sqlConnection.QueryAsync( + "[dbo].[OrganizationInstallation_ReadByInstallationId]", + new { InstallationId = installationId }, + commandType: CommandType.StoredProcedure); + + return results.FirstOrDefault(); + } + + public async Task> GetByOrganizationIdAsync(Guid organizationId) + { + var sqlConnection = new SqlConnection(ConnectionString); + + var results = await sqlConnection.QueryAsync( + "[dbo].[OrganizationInstallation_ReadByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + return results.ToArray(); + } +} diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index c873f84aa0..834f681d28 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -63,6 +63,7 @@ public static class DapperServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); if (selfHosted) { diff --git a/src/Infrastructure.EntityFramework/Billing/Configurations/OrganizationInstallationEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/Billing/Configurations/OrganizationInstallationEntityTypeConfiguration.cs new file mode 100644 index 0000000000..e4ba27b75d --- /dev/null +++ b/src/Infrastructure.EntityFramework/Billing/Configurations/OrganizationInstallationEntityTypeConfiguration.cs @@ -0,0 +1,29 @@ +using Bit.Infrastructure.EntityFramework.Billing.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Bit.Infrastructure.EntityFramework.Billing.Configurations; + +public class OrganizationInstallationEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .Property(oi => oi.Id) + .ValueGeneratedNever(); + + builder + .HasKey(oi => oi.Id) + .IsClustered(); + + builder + .HasIndex(oi => oi.OrganizationId) + .IsClustered(false); + + builder + .HasIndex(oi => oi.InstallationId) + .IsClustered(false); + + builder.ToTable(nameof(OrganizationInstallation)); + } +} diff --git a/src/Infrastructure.EntityFramework/Billing/Models/OrganizationInstallation.cs b/src/Infrastructure.EntityFramework/Billing/Models/OrganizationInstallation.cs new file mode 100644 index 0000000000..2f00768206 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Billing/Models/OrganizationInstallation.cs @@ -0,0 +1,19 @@ +using AutoMapper; +using Bit.Infrastructure.EntityFramework.AdminConsole.Models; +using Bit.Infrastructure.EntityFramework.Models; + +namespace Bit.Infrastructure.EntityFramework.Billing.Models; + +public class OrganizationInstallation : Core.Billing.Entities.OrganizationInstallation +{ + public virtual Installation Installation { get; set; } + public virtual Organization Organization { get; set; } +} + +public class OrganizationInstallationMapperProfile : Profile +{ + public OrganizationInstallationMapperProfile() + { + CreateMap().ReverseMap(); + } +} diff --git a/src/Infrastructure.EntityFramework/Billing/Repositories/OrganizationInstallationRepository.cs b/src/Infrastructure.EntityFramework/Billing/Repositories/OrganizationInstallationRepository.cs new file mode 100644 index 0000000000..566c52332e --- /dev/null +++ b/src/Infrastructure.EntityFramework/Billing/Repositories/OrganizationInstallationRepository.cs @@ -0,0 +1,45 @@ +using AutoMapper; +using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using EFOrganizationInstallation = Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation; + +namespace Bit.Infrastructure.EntityFramework.Billing.Repositories; + +public class OrganizationInstallationRepository( + IMapper mapper, + IServiceScopeFactory serviceScopeFactory) : Repository( + serviceScopeFactory, + mapper, + context => context.OrganizationInstallations), IOrganizationInstallationRepository +{ + public async Task GetByInstallationIdAsync(Guid installationId) + { + using var serviceScope = ServiceScopeFactory.CreateScope(); + + var databaseContext = GetDatabaseContext(serviceScope); + + var query = + from organizationInstallation in databaseContext.OrganizationInstallations + where organizationInstallation.Id == installationId + select organizationInstallation; + + return await query.FirstOrDefaultAsync(); + } + + public async Task> GetByOrganizationIdAsync(Guid organizationId) + { + using var serviceScope = ServiceScopeFactory.CreateScope(); + + var databaseContext = GetDatabaseContext(serviceScope); + + var query = + from organizationInstallation in databaseContext.OrganizationInstallations + where organizationInstallation.OrganizationId == organizationId + select organizationInstallation; + + return await query.ToArrayAsync(); + } +} diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs index 1f1ea16bfc..24ef2ab269 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs @@ -78,6 +78,7 @@ public class DatabaseContext : DbContext public DbSet ClientOrganizationMigrationRecords { get; set; } public DbSet PasswordHealthReportApplications { get; set; } public DbSet SecurityTasks { get; set; } + public DbSet OrganizationInstallations { get; set; } protected override void OnModelCreating(ModelBuilder builder) { diff --git a/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_Create.sql b/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_Create.sql new file mode 100644 index 0000000000..2bca369fc9 --- /dev/null +++ b/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_Create.sql @@ -0,0 +1,27 @@ +CREATE PROCEDURE [dbo].[OrganizationInstallation_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @InstallationId UNIQUEIDENTIFIER, + @CreationDate DATETIME2 (7), + @RevisionDate DATETIME2 (7) = NULL +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[OrganizationInstallation] + ( + [Id], + [OrganizationId], + [InstallationId], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @OrganizationId, + @InstallationId, + @CreationDate, + @RevisionDate + ) +END diff --git a/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_DeleteById.sql b/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_DeleteById.sql new file mode 100644 index 0000000000..edc97a1a05 --- /dev/null +++ b/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_DeleteById.sql @@ -0,0 +1,12 @@ +CREATE PROCEDURE [dbo].[OrganizationInstallation_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE + FROM + [dbo].[OrganizationInstallation] + WHERE + [Id] = @Id +END diff --git a/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadById.sql b/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadById.sql new file mode 100644 index 0000000000..bda3039cf9 --- /dev/null +++ b/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadById.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationInstallationView] + WHERE + [Id] = @Id +END diff --git a/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadByInstallationId.sql b/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadByInstallationId.sql new file mode 100644 index 0000000000..a2a3b2ef16 --- /dev/null +++ b/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadByInstallationId.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadByInstallationId] + @InstallationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationInstallationView] + WHERE + [InstallationId] = @InstallationId +END diff --git a/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadByOrganizationId.sql b/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadByOrganizationId.sql new file mode 100644 index 0000000000..3dffe1968e --- /dev/null +++ b/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadByOrganizationId.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationInstallationView] + WHERE + [OrganizationId] = @OrganizationId +END diff --git a/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_Update.sql b/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_Update.sql new file mode 100644 index 0000000000..22aefc540d --- /dev/null +++ b/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_Update.sql @@ -0,0 +1,17 @@ +CREATE PROCEDURE [dbo].[OrganizationInstallation_Update] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @InstallationId UNIQUEIDENTIFIER, + @CreationDate DATETIME2 (7), + @RevisionDate DATETIME2 (7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[OrganizationInstallation] + SET + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id +END diff --git a/src/Sql/Billing/dbo/Tables/OrganizationInstallation.sql b/src/Sql/Billing/dbo/Tables/OrganizationInstallation.sql new file mode 100644 index 0000000000..e17d689a9a --- /dev/null +++ b/src/Sql/Billing/dbo/Tables/OrganizationInstallation.sql @@ -0,0 +1,18 @@ +CREATE TABLE [dbo].[OrganizationInstallation] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [InstallationId] UNIQUEIDENTIFIER NOT NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [RevisionDate] DATETIME2 (7) NULL, + CONSTRAINT [PK_OrganizationInstallation] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_OrganizationInstallation_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE, + CONSTRAINT [FK_OrganizationInstallation_Installation] FOREIGN KEY ([InstallationId]) REFERENCES [dbo].[Installation] ([Id]) ON DELETE CASCADE +); +GO + +CREATE NONCLUSTERED INDEX [IX_OrganizationInstallation_OrganizationId] + ON [dbo].[OrganizationInstallation]([OrganizationId] ASC); +GO + +CREATE NONCLUSTERED INDEX [IX_OrganizationInstallation_InstallationId] + ON [dbo].[OrganizationInstallation]([InstallationId] ASC); diff --git a/src/Sql/Billing/dbo/Views/OrganizationInstallationView.sql b/src/Sql/Billing/dbo/Views/OrganizationInstallationView.sql new file mode 100644 index 0000000000..c68142b700 --- /dev/null +++ b/src/Sql/Billing/dbo/Views/OrganizationInstallationView.sql @@ -0,0 +1,6 @@ +CREATE VIEW [dbo].[OrganizationInstallationView] +AS +SELECT + * +FROM + [dbo].[OrganizationInstallation]; diff --git a/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs index ec6047fbfe..483d962830 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs @@ -10,6 +10,7 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Services; +using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; @@ -47,6 +48,7 @@ public class OrganizationsControllerTests : IDisposable private readonly IReferenceEventService _referenceEventService; private readonly ISubscriberService _subscriberService; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; + private readonly IOrganizationInstallationRepository _organizationInstallationRepository; private readonly OrganizationsController _sut; @@ -70,6 +72,7 @@ public class OrganizationsControllerTests : IDisposable _referenceEventService = Substitute.For(); _subscriberService = Substitute.For(); _removeOrganizationUserCommand = Substitute.For(); + _organizationInstallationRepository = Substitute.For(); _sut = new OrganizationsController( _organizationRepository, @@ -85,7 +88,8 @@ public class OrganizationsControllerTests : IDisposable _upgradeOrganizationPlanCommand, _addSecretsManagerSubscriptionCommand, _referenceEventService, - _subscriberService); + _subscriberService, + _organizationInstallationRepository); } public void Dispose() diff --git a/util/Migrator/DbScripts/2024-11-26_00_AddTable_OrganizationInstallation.sql b/util/Migrator/DbScripts/2024-11-26_00_AddTable_OrganizationInstallation.sql new file mode 100644 index 0000000000..20199ade6a --- /dev/null +++ b/util/Migrator/DbScripts/2024-11-26_00_AddTable_OrganizationInstallation.sql @@ -0,0 +1,158 @@ +-- OrganizationInstallation + +-- Table +IF OBJECT_ID('[dbo].[OrganizationInstallation]') IS NULL +BEGIN + CREATE TABLE [dbo].[OrganizationInstallation] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [InstallationId] UNIQUEIDENTIFIER NOT NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [RevisionDate] DATETIME2 (7) NULL, + CONSTRAINT [PK_OrganizationInstallation] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_OrganizationInstallation_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE, + CONSTRAINT [FK_OrganizationInstallation_Installation] FOREIGN KEY ([InstallationId]) REFERENCES [dbo].[Installation] ([Id]) ON DELETE CASCADE + ); +END +GO + +-- Indexes +IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationInstallation_OrganizationId') +BEGIN + CREATE NONCLUSTERED INDEX [IX_OrganizationInstallation_OrganizationId] + ON [dbo].[OrganizationInstallation]([OrganizationId] ASC); +END + +IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationInstallation_InstallationId') +BEGIN + CREATE NONCLUSTERED INDEX [IX_OrganizationInstallation_InstallationId] + ON [dbo].[OrganizationInstallation]([InstallationId] ASC); +END + +-- View +IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationInstallationView') +BEGIN + DROP VIEW [dbo].[OrganizationInstallationView]; +END +GO + +CREATE VIEW [dbo].[OrganizationInstallationView] +AS +SELECT + * +FROM + [dbo].[OrganizationInstallation] +GO + +-- Stored Procedures: Create +CREATE OR ALTER PROCEDURE [dbo].[OrganizationInstallation_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @InstallationId UNIQUEIDENTIFIER, + @CreationDate DATETIME2 (7), + @RevisionDate DATETIME2 (7) = NULL +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[OrganizationInstallation] + ( + [Id], + [OrganizationId], + [InstallationId], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @OrganizationId, + @InstallationId, + @CreationDate, + @RevisionDate + ) +END +GO + +-- Stored Procedures: DeleteById +CREATE OR ALTER PROCEDURE [dbo].[OrganizationInstallation_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE + FROM + [dbo].[OrganizationInstallation] + WHERE + [Id] = @Id +END +GO + +-- Stored Procedures: ReadById +CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationInstallationView] + WHERE + [Id] = @Id +END +GO + +-- Stored Procedures: ReadByInstallationId +CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadByInstallationId] + @InstallationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationInstallationView] + WHERE + [InstallationId] = @InstallationId +END +GO + +-- Stored Procedures: ReadByOrganizationId +CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationInstallationView] + WHERE + [OrganizationId] = @OrganizationId +END +GO + +-- Stored Procedures: Update +CREATE PROCEDURE [dbo].[OrganizationInstallation_Update] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @InstallationId UNIQUEIDENTIFIER, + @CreationDate DATETIME2 (7), + @RevisionDate DATETIME2 (7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[OrganizationInstallation] + SET + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id +END +GO diff --git a/util/MySqlMigrations/Migrations/20241126185456_AddTable_OrganizationInstallation.Designer.cs b/util/MySqlMigrations/Migrations/20241126185456_AddTable_OrganizationInstallation.Designer.cs new file mode 100644 index 0000000000..26cc7988b4 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20241126185456_AddTable_OrganizationInstallation.Designer.cs @@ -0,0 +1,2988 @@ +// +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("20241126185456_AddTable_OrganizationInstallation")] + partial class AddTable_OrganizationInstallation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionCreationDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasColumnType("longtext"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AaGuid") + .HasColumnType("char(36)"); + + b.Property("Counter") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasColumnType("longtext"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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.Models.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.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + 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/20241126185456_AddTable_OrganizationInstallation.cs b/util/MySqlMigrations/Migrations/20241126185456_AddTable_OrganizationInstallation.cs new file mode 100644 index 0000000000..ddffc19aff --- /dev/null +++ b/util/MySqlMigrations/Migrations/20241126185456_AddTable_OrganizationInstallation.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddTable_OrganizationInstallation : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OrganizationInstallation", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + OrganizationId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + InstallationId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + CreationDate = table.Column(type: "datetime(6)", nullable: false), + RevisionDate = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationInstallation", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationInstallation_Installation_InstallationId", + column: x => x.InstallationId, + principalTable: "Installation", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_OrganizationInstallation_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationInstallation_InstallationId", + table: "OrganizationInstallation", + column: "InstallationId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationInstallation_OrganizationId", + table: "OrganizationInstallation", + column: "OrganizationId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrganizationInstallation"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 5927762791..000274387c 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -737,6 +737,35 @@ namespace Bit.MySqlMigrations.Migrations b.ToTable("ClientOrganizationMigrationRecord", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => { b.Property("Id") @@ -2336,6 +2365,25 @@ namespace Bit.MySqlMigrations.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.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") diff --git a/util/PostgresMigrations/Migrations/20241126185450_AddTable_OrganizationInstallation.Designer.cs b/util/PostgresMigrations/Migrations/20241126185450_AddTable_OrganizationInstallation.Designer.cs new file mode 100644 index 0000000000..d511ef53ef --- /dev/null +++ b/util/PostgresMigrations/Migrations/20241126185450_AddTable_OrganizationInstallation.Designer.cs @@ -0,0 +1,2994 @@ +// +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("20241126185450_AddTable_OrganizationInstallation")] + partial class AddTable_OrganizationInstallation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionCreationDeletion") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasColumnType("text"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MasterPasswordHash") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("OrganizationId", "ExternalId"), new[] { "UserId" }); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("Counter") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsPrf") + .HasColumnType("boolean"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasColumnType("text"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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.Models.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.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + 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/20241126185450_AddTable_OrganizationInstallation.cs b/util/PostgresMigrations/Migrations/20241126185450_AddTable_OrganizationInstallation.cs new file mode 100644 index 0000000000..e653db6c25 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20241126185450_AddTable_OrganizationInstallation.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddTable_OrganizationInstallation : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OrganizationInstallation", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrganizationId = table.Column(type: "uuid", nullable: false), + InstallationId = table.Column(type: "uuid", nullable: false), + CreationDate = table.Column(type: "timestamp with time zone", nullable: false), + RevisionDate = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationInstallation", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationInstallation_Installation_InstallationId", + column: x => x.InstallationId, + principalTable: "Installation", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_OrganizationInstallation_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationInstallation_InstallationId", + table: "OrganizationInstallation", + column: "InstallationId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationInstallation_OrganizationId", + table: "OrganizationInstallation", + column: "OrganizationId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrganizationInstallation"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 4259d1aed8..c8cce33e11 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -742,6 +742,35 @@ namespace Bit.PostgresMigrations.Migrations b.ToTable("ClientOrganizationMigrationRecord", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => { b.Property("Id") @@ -2342,6 +2371,25 @@ namespace Bit.PostgresMigrations.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.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") diff --git a/util/SqliteMigrations/Migrations/20241126185500_AddTable_OrganizationInstallation.Designer.cs b/util/SqliteMigrations/Migrations/20241126185500_AddTable_OrganizationInstallation.Designer.cs new file mode 100644 index 0000000000..85f073ae90 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20241126185500_AddTable_OrganizationInstallation.Designer.cs @@ -0,0 +1,2977 @@ +// +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("20241126185500_AddTable_OrganizationInstallation")] + partial class AddTable_OrganizationInstallation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionCreationDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResponseDate") + .HasColumnType("TEXT"); + + b.Property("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GranteeId") + .HasColumnType("TEXT"); + + b.Property("GrantorId") + .HasColumnType("TEXT"); + + b.Property("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WaitTimeDays") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AaGuid") + .HasColumnType("TEXT"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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.Models.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.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + 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/20241126185500_AddTable_OrganizationInstallation.cs b/util/SqliteMigrations/Migrations/20241126185500_AddTable_OrganizationInstallation.cs new file mode 100644 index 0000000000..fde1b974d5 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20241126185500_AddTable_OrganizationInstallation.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddTable_OrganizationInstallation : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OrganizationInstallation", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + InstallationId = table.Column(type: "TEXT", nullable: false), + CreationDate = table.Column(type: "TEXT", nullable: false), + RevisionDate = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationInstallation", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationInstallation_Installation_InstallationId", + column: x => x.InstallationId, + principalTable: "Installation", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_OrganizationInstallation_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationInstallation_InstallationId", + table: "OrganizationInstallation", + column: "InstallationId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationInstallation_OrganizationId", + table: "OrganizationInstallation", + column: "OrganizationId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrganizationInstallation"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index f906543254..12a1c9792b 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -726,6 +726,35 @@ namespace Bit.SqliteMigrations.Migrations b.ToTable("ClientOrganizationMigrationRecord", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => { b.Property("Id") @@ -2325,6 +2354,25 @@ namespace Bit.SqliteMigrations.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.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") From c852575a9eaf67789eb0bbb0c80b10ff7de018b8 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 12 Dec 2024 07:08:17 -0500 Subject: [PATCH 63/94] [PM-14984] Use provider subscription for MSP managed enterprise license (#5102) * Use provider subscription when creating license for MSP managed enterprise organization * Run dotnet format --- .../Cloud/CloudGetOrganizationLicenseQuery.cs | 20 +++++++++-- .../CloudGetOrganizationLicenseQueryTests.cs | 33 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs b/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs index a4b08736c2..d7782fcd98 100644 --- a/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs +++ b/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs @@ -1,4 +1,6 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; @@ -12,15 +14,18 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer private readonly IInstallationRepository _installationRepository; private readonly IPaymentService _paymentService; private readonly ILicensingService _licensingService; + private readonly IProviderRepository _providerRepository; public CloudGetOrganizationLicenseQuery( IInstallationRepository installationRepository, IPaymentService paymentService, - ILicensingService licensingService) + ILicensingService licensingService, + IProviderRepository providerRepository) { _installationRepository = installationRepository; _paymentService = paymentService; _licensingService = licensingService; + _providerRepository = providerRepository; } public async Task GetLicenseAsync(Organization organization, Guid installationId, @@ -32,11 +37,22 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer throw new BadRequestException("Invalid installation id"); } - var subscriptionInfo = await _paymentService.GetSubscriptionAsync(organization); + var subscriptionInfo = await GetSubscriptionAsync(organization); return new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version) { Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo) }; } + + private async Task GetSubscriptionAsync(Organization organization) + { + if (organization is not { Status: OrganizationStatusType.Managed }) + { + return await _paymentService.GetSubscriptionAsync(organization); + } + + var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id); + return await _paymentService.GetSubscriptionAsync(provider); + } } diff --git a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs index 00a4b12b2e..52bee7068f 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs @@ -1,4 +1,6 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -11,6 +13,7 @@ using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using NSubstitute.ReturnsExtensions; +using Stripe; using Xunit; namespace Bit.Core.Test.OrganizationFeatures.OrganizationLicenses; @@ -62,4 +65,34 @@ public class CloudGetOrganizationLicenseQueryTests Assert.Equal(installationId, result.InstallationId); Assert.Equal(licenseSignature, result.SignatureBytes); } + + [Theory] + [BitAutoData] + public async Task GetLicenseAsync_MSPManagedOrganization_UsesProviderSubscription(SutProvider sutProvider, + Organization organization, Guid installationId, Installation installation, SubscriptionInfo subInfo, + byte[] licenseSignature, Provider provider) + { + organization.Status = OrganizationStatusType.Managed; + organization.ExpirationDate = null; + + subInfo.Subscription = new SubscriptionInfo.BillingSubscription(new Subscription + { + CurrentPeriodStart = DateTime.UtcNow, + CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) + }); + + installation.Enabled = true; + sutProvider.GetDependency().GetByIdAsync(installationId).Returns(installation); + sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id).Returns(provider); + sutProvider.GetDependency().GetSubscriptionAsync(provider).Returns(subInfo); + sutProvider.GetDependency().SignLicense(Arg.Any()).Returns(licenseSignature); + + var result = await sutProvider.Sut.GetLicenseAsync(organization, installationId); + + Assert.Equal(LicenseType.Organization, result.LicenseType); + Assert.Equal(organization.Id, result.Id); + Assert.Equal(installationId, result.InstallationId); + Assert.Equal(licenseSignature, result.SignatureBytes); + Assert.Equal(DateTime.UtcNow.AddYears(1).Date, result.Expires!.Value.Date); + } } From a76a9cb8001f8cd54d2d89cb9f45ad26bb71d76a Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Thu, 12 Dec 2024 10:18:11 -0500 Subject: [PATCH 64/94] [PM-14826] Add UsePolicies check to GET endpoints (#5046) GetByToken and GetMasterPasswordPolicy endpoints provide policy information, so if the organization is not using policies, then we avoid the rest of the logic. --- .../Controllers/PoliciesController.cs | 34 ++- .../Controllers/PoliciesControllerTests.cs | 278 +++++++++++++++++- 2 files changed, 291 insertions(+), 21 deletions(-) diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs index 1167d7a86c..7de6f6e730 100644 --- a/src/Api/AdminConsole/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs @@ -27,19 +27,20 @@ namespace Bit.Api.AdminConsole.Controllers; [Authorize("Application")] public class PoliciesController : Controller { - private readonly IPolicyRepository _policyRepository; - private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IUserService _userService; private readonly ICurrentContext _currentContext; - private readonly GlobalSettings _globalSettings; - private readonly IDataProtector _organizationServiceDataProtector; - private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; private readonly IFeatureService _featureService; + private readonly GlobalSettings _globalSettings; private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; + private readonly IOrganizationRepository _organizationRepository; + private readonly IDataProtector _organizationServiceDataProtector; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; + private readonly IPolicyRepository _policyRepository; + private readonly IUserService _userService; + private readonly ISavePolicyCommand _savePolicyCommand; - public PoliciesController( - IPolicyRepository policyRepository, + public PoliciesController(IPolicyRepository policyRepository, IOrganizationUserRepository organizationUserRepository, IUserService userService, ICurrentContext currentContext, @@ -48,6 +49,7 @@ public class PoliciesController : Controller IDataProtectorTokenFactory orgUserInviteTokenDataFactory, IFeatureService featureService, IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery, + IOrganizationRepository organizationRepository, ISavePolicyCommand savePolicyCommand) { _policyRepository = policyRepository; @@ -57,7 +59,7 @@ public class PoliciesController : Controller _globalSettings = globalSettings; _organizationServiceDataProtector = dataProtectionProvider.CreateProtector( "OrganizationServiceDataProtector"); - + _organizationRepository = organizationRepository; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; _featureService = featureService; _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; @@ -104,6 +106,13 @@ public class PoliciesController : Controller public async Task> GetByToken(Guid orgId, [FromQuery] string email, [FromQuery] string token, [FromQuery] Guid organizationUserId) { + var organization = await _organizationRepository.GetByIdAsync(orgId); + + if (organization is not { UsePolicies: true }) + { + throw new NotFoundException(); + } + // TODO: PM-4142 - remove old token validation logic once 3 releases of backwards compatibility are complete var newTokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken( _orgUserInviteTokenDataFactory, token, organizationUserId, email); @@ -158,6 +167,13 @@ public class PoliciesController : Controller [HttpGet("master-password")] public async Task GetMasterPasswordPolicy(Guid orgId) { + var organization = await _organizationRepository.GetByIdAsync(orgId); + + if (organization is not { UsePolicies: true }) + { + throw new NotFoundException(); + } + var userId = _userService.GetProperUserId(User).Value; var orgUser = await _organizationUserRepository.GetByOrganizationAsync(orgId, userId); diff --git a/test/Api.Test/Controllers/PoliciesControllerTests.cs b/test/Api.Test/Controllers/PoliciesControllerTests.cs index 1b96ace5d0..1f652c80f5 100644 --- a/test/Api.Test/Controllers/PoliciesControllerTests.cs +++ b/test/Api.Test/Controllers/PoliciesControllerTests.cs @@ -6,11 +6,13 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Tokens; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -28,10 +30,19 @@ public class PoliciesControllerTests [Theory] [BitAutoData] public async Task GetMasterPasswordPolicy_WhenCalled_ReturnsMasterPasswordPolicy( - SutProvider sutProvider, Guid orgId, Guid userId, OrganizationUser orgUser, - Policy policy, MasterPasswordPolicyData mpPolicyData) + SutProvider sutProvider, + Guid orgId, Guid userId, + OrganizationUser orgUser, + Policy policy, + MasterPasswordPolicyData mpPolicyData, + Organization organization) { // Arrange + organization.UsePolicies = true; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(organization); + sutProvider.GetDependency() .GetProperUserId(Arg.Any()) .Returns((Guid?)userId); @@ -135,6 +146,39 @@ public class PoliciesControllerTests await Assert.ThrowsAsync(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId)); } + [Theory] + [BitAutoData] + public async Task GetMasterPasswordPolicy_WhenUsePoliciesIsFalse_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns((Organization)null); + + + // Act & Assert + await Assert.ThrowsAsync(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId)); + } + + [Theory] + [BitAutoData] + public async Task GetMasterPasswordPolicy_WhenOrgIsNull_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId, + Organization organization) + { + // Arrange + organization.UsePolicies = false; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(organization); + + + // Act & Assert + await Assert.ThrowsAsync(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId)); + } + [Theory] [BitAutoData] public async Task Get_WhenUserCanManagePolicies_WithExistingType_ReturnsExistingPolicy( @@ -142,16 +186,16 @@ public class PoliciesControllerTests { // Arrange sutProvider.GetDependency() - .ManagePolicies(orgId) - .Returns(true); + .ManagePolicies(orgId) + .Returns(true); policy.Type = (PolicyType)type; policy.Enabled = true; policy.Data = null; sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(orgId, (PolicyType)type) - .Returns(policy); + .GetByOrganizationIdTypeAsync(orgId, (PolicyType)type) + .Returns(policy); // Act var result = await sutProvider.Sut.Get(orgId, type); @@ -171,12 +215,12 @@ public class PoliciesControllerTests { // Arrange sutProvider.GetDependency() - .ManagePolicies(orgId) - .Returns(true); + .ManagePolicies(orgId) + .Returns(true); sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(orgId, (PolicyType)type) - .Returns((Policy)null); + .GetByOrganizationIdTypeAsync(orgId, (PolicyType)type) + .Returns((Policy)null); // Act var result = await sutProvider.Sut.Get(orgId, type); @@ -194,11 +238,221 @@ public class PoliciesControllerTests { // Arrange sutProvider.GetDependency() - .ManagePolicies(orgId) - .Returns(false); + .ManagePolicies(orgId) + .Returns(false); // Act & Assert await Assert.ThrowsAsync(() => sutProvider.Sut.Get(orgId, type)); } + [Theory] + [BitAutoData] + public async Task GetByToken_WhenOrganizationUseUsePoliciesIsFalse_ThrowsNotFoundException( + SutProvider sutProvider, Guid orgId, Guid organizationUserId, string token, string email, + Organization organization) + { + // Arrange + organization.UsePolicies = false; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(organization); + + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId)); + } + + [Theory] + [BitAutoData] + public async Task GetByToken_WhenOrganizationIsNull_ThrowsNotFoundException( + SutProvider sutProvider, Guid orgId, Guid organizationUserId, string token, string email) + { + // Arrange + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns((Organization)null); + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId)); + } + + [Theory] + [BitAutoData] + public async Task GetByToken_WhenTokenIsInvalid_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId, + Guid organizationUserId, + string token, + string email, + Organization organization + ) + { + // Arrange + organization.UsePolicies = true; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(organization); + + var decryptedToken = Substitute.For(); + decryptedToken.Valid.Returns(false); + + var orgUserInviteTokenDataFactory = sutProvider.GetDependency>(); + + orgUserInviteTokenDataFactory.TryUnprotect(token, out Arg.Any()) + .Returns(x => + { + x[1] = decryptedToken; + return true; + }); + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId)); + } + + [Theory] + [BitAutoData] + public async Task GetByToken_WhenUserIsNull_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId, + Guid organizationUserId, + string token, + string email, + Organization organization + ) + { + // Arrange + organization.UsePolicies = true; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(organization); + + var decryptedToken = Substitute.For(); + decryptedToken.Valid.Returns(true); + decryptedToken.OrgUserId = organizationUserId; + decryptedToken.OrgUserEmail = email; + + var orgUserInviteTokenDataFactory = sutProvider.GetDependency>(); + + orgUserInviteTokenDataFactory.TryUnprotect(token, out Arg.Any()) + .Returns(x => + { + x[1] = decryptedToken; + return true; + }); + + sutProvider.GetDependency() + .GetByIdAsync(organizationUserId) + .Returns((OrganizationUser)null); + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId)); + } + + [Theory] + [BitAutoData] + public async Task GetByToken_WhenUserOrgIdDoesNotMatchOrgId_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId, + Guid organizationUserId, + string token, + string email, + OrganizationUser orgUser, + Organization organization + ) + { + // Arrange + organization.UsePolicies = true; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(organization); + + var decryptedToken = Substitute.For(); + decryptedToken.Valid.Returns(true); + decryptedToken.OrgUserId = organizationUserId; + decryptedToken.OrgUserEmail = email; + + var orgUserInviteTokenDataFactory = sutProvider.GetDependency>(); + + orgUserInviteTokenDataFactory.TryUnprotect(token, out Arg.Any()) + .Returns(x => + { + x[1] = decryptedToken; + return true; + }); + + orgUser.OrganizationId = Guid.Empty; + + sutProvider.GetDependency() + .GetByIdAsync(organizationUserId) + .Returns(orgUser); + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId)); + } + + [Theory] + [BitAutoData] + public async Task GetByToken_ShouldReturnEnabledPolicies( + SutProvider sutProvider, + Guid orgId, + Guid organizationUserId, + string token, + string email, + OrganizationUser orgUser, + Organization organization + ) + { + // Arrange + organization.UsePolicies = true; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(organization); + + var decryptedToken = Substitute.For(); + decryptedToken.Valid.Returns(true); + decryptedToken.OrgUserId = organizationUserId; + decryptedToken.OrgUserEmail = email; + + var orgUserInviteTokenDataFactory = sutProvider.GetDependency>(); + + orgUserInviteTokenDataFactory.TryUnprotect(token, out Arg.Any()) + .Returns(x => + { + x[1] = decryptedToken; + return true; + }); + + orgUser.OrganizationId = orgId; + sutProvider.GetDependency() + .GetByIdAsync(organizationUserId) + .Returns(orgUser); + + var enabledPolicy = Substitute.For(); + enabledPolicy.Enabled = true; + var disabledPolicy = Substitute.For(); + disabledPolicy.Enabled = false; + + var policies = new[] { enabledPolicy, disabledPolicy }; + + + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(orgId) + .Returns(policies); + + // Act + var result = await sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId); + + // Assert + var expectedPolicy = result.Data.Single(); + + Assert.NotNull(result); + + Assert.Equal(enabledPolicy.Id, expectedPolicy.Id); + Assert.Equal(enabledPolicy.Type, expectedPolicy.Type); + Assert.Equal(enabledPolicy.Enabled, expectedPolicy.Enabled); + } } From 867fa848dd8c15cdd1a7e65716799d4bd2b52d25 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:08:11 -0800 Subject: [PATCH 65/94] [PM-8220] New Device Verification (#5084) * feat(BaseRequestValidator): Add global setting for new device verification. Refactor BaseRequestValidator enabling better self-documenting code and better single responsibility principle for validators. Updated DeviceValidator to handle new device verification, behind a feature flag. Moved IDeviceValidator interface to separate file. Updated CustomRequestValidator to act as the conduit by which *Validators communicate authentication context between themselves and the RequestValidators. Adding new test for DeviceValidator class. Updated tests for BaseRequestValidator as some functionality was moved to the DeviceValidator class. --- .../Implementations/RegisterUserCommand.cs | 2 +- src/Core/Settings/GlobalSettings.cs | 13 +- src/Core/Settings/IGlobalSettings.cs | 1 + .../CustomValidatorRequestContext.cs | 32 + .../Enums/DeviceValidationResultType.cs | 10 + .../RequestValidators/BaseRequestValidator.cs | 201 ++++-- .../CustomTokenRequestValidator.cs | 18 +- .../RequestValidators/DeviceValidator.cs | 222 ++++-- .../RequestValidators/IDeviceValidator.cs | 24 + .../ResourceOwnerPasswordValidator.cs | 36 +- .../WebAuthnGrantValidator.cs | 26 +- .../ResourceOwnerPasswordValidatorTests.cs | 57 +- .../BaseRequestValidatorTests.cs | 276 +++++--- .../IdentityServer/DeviceValidatorTests.cs | 662 +++++++++++++----- .../BaseRequestValidatorTestWrapper.cs | 5 + 15 files changed, 1112 insertions(+), 473 deletions(-) create mode 100644 src/Identity/IdentityServer/Enums/DeviceValidationResultType.cs create mode 100644 src/Identity/IdentityServer/RequestValidators/IDeviceValidator.cs diff --git a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs index 8174d7d364..89851fce23 100644 --- a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs +++ b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs @@ -329,7 +329,7 @@ public class RegisterUserCommand : IRegisterUserCommand { // We validate open registration on send of initial email and here b/c a user could technically start the // account creation process while open registration is enabled and then finish it after it has been - // disabled by the self hosted admin.Ï + // disabled by the self hosted admin. if (_globalSettings.DisableUserRegistration) { throw new BadRequestException(_disabledUserRegistrationExceptionMsg); diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 793b6ac1c1..2ececb9658 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -41,6 +41,7 @@ public class GlobalSettings : IGlobalSettings public virtual string HibpApiKey { get; set; } public virtual bool DisableUserRegistration { get; set; } public virtual bool DisableEmailNewDevice { get; set; } + public virtual bool EnableNewDeviceVerification { get; set; } public virtual bool EnableCloudCommunication { get; set; } = false; public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days public virtual string EventGridKey { get; set; } @@ -433,18 +434,18 @@ public class GlobalSettings : IGlobalSettings public bool EnableSendTracing { get; set; } = false; /// /// The date and time at which registration will be enabled. - /// + /// /// **This value should not be updated once set, as it is used to determine installation location of devices.** - /// + /// /// If null, registration is disabled. - /// + /// /// public DateTime? RegistrationStartDate { get; set; } /// /// The date and time at which registration will be disabled. - /// + /// /// **This value should not be updated once set, as it is used to determine installation location of devices.** - /// + /// /// If null, hub registration has no yet known expiry. /// public DateTime? RegistrationEndDate { get; set; } @@ -454,7 +455,7 @@ public class GlobalSettings : IGlobalSettings { /// /// List of Notification Hub settings to use for sending push notifications. - /// + /// /// Note that hubs on the same namespace share active device limits, so multiple namespaces should be used to increase capacity. /// public List NotificationHubs { get; set; } = new(); diff --git a/src/Core/Settings/IGlobalSettings.cs b/src/Core/Settings/IGlobalSettings.cs index d91d4b8c3d..02d151ed95 100644 --- a/src/Core/Settings/IGlobalSettings.cs +++ b/src/Core/Settings/IGlobalSettings.cs @@ -14,6 +14,7 @@ public interface IGlobalSettings string LicenseCertificatePassword { get; set; } int OrganizationInviteExpirationHours { get; set; } bool DisableUserRegistration { get; set; } + bool EnableNewDeviceVerification { get; set; } IInstallationSettings Installation { get; set; } IFileStorageSettings Attachment { get; set; } IConnectionStringSettings Storage { get; set; } diff --git a/src/Identity/IdentityServer/CustomValidatorRequestContext.cs b/src/Identity/IdentityServer/CustomValidatorRequestContext.cs index a3485bfb13..bce460c5c4 100644 --- a/src/Identity/IdentityServer/CustomValidatorRequestContext.cs +++ b/src/Identity/IdentityServer/CustomValidatorRequestContext.cs @@ -1,11 +1,43 @@ using Bit.Core.Auth.Models.Business; using Bit.Core.Entities; +using Duende.IdentityServer.Validation; namespace Bit.Identity.IdentityServer; public class CustomValidatorRequestContext { public User User { get; set; } + /// + /// This is the device that the user is using to authenticate. It can be either known or unknown. + /// We set it here since the ResourceOwnerPasswordValidator needs the device to know if CAPTCHA is required. + /// The option to set it here saves a trip to the database. + /// + public Device Device { get; set; } + /// + /// Communicates whether or not the device in the request is known to the user. + /// KnownDevice is set in the child classes of the BaseRequestValidator using the DeviceValidator.KnownDeviceAsync method. + /// Except in the CustomTokenRequestValidator, where it is hardcoded to true. + /// public bool KnownDevice { get; set; } + /// + /// This communicates whether or not two factor is required for the user to authenticate. + /// + public bool TwoFactorRequired { get; set; } = false; + /// + /// This communicates whether or not SSO is required for the user to authenticate. + /// + public bool SsoRequired { get; set; } = false; + /// + /// We use the parent class for both GrantValidationResult and TokenRequestValidationResult here for + /// flexibility when building an error response. + /// This will be null if the authentication request is successful. + /// + public ValidationResult ValidationErrorResult { get; set; } + /// + /// This dictionary should contain relevant information for the clients to act on. + /// This will contain the information used to guide a user to successful authentication, such as TwoFactorProviders. + /// This will be null if the authentication request is successful. + /// + public Dictionary CustomResponse { get; set; } public CaptchaResponse CaptchaResponse { get; set; } } diff --git a/src/Identity/IdentityServer/Enums/DeviceValidationResultType.cs b/src/Identity/IdentityServer/Enums/DeviceValidationResultType.cs new file mode 100644 index 0000000000..45c901e306 --- /dev/null +++ b/src/Identity/IdentityServer/Enums/DeviceValidationResultType.cs @@ -0,0 +1,10 @@ +namespace Bit.Identity.IdentityServer.Enums; + +public enum DeviceValidationResultType : byte +{ + Success = 0, + InvalidUser = 1, + InvalidNewDeviceOtp = 2, + NewDeviceVerificationRequired = 3, + NoDeviceInformationProvided = 4 +} diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index 185d32a7f2..78c00f86d5 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -77,37 +77,51 @@ public abstract class BaseRequestValidator where T : class protected async Task ValidateAsync(T context, ValidatedTokenRequest request, CustomValidatorRequestContext validatorContext) { + // 1. we need to check if the user is a bot and if their master password hash is correct var isBot = validatorContext.CaptchaResponse?.IsBot ?? false; - if (isBot) - { - _logger.LogInformation(Constants.BypassFiltersEventId, - "Login attempt for {0} detected as a captcha bot with score {1}.", - request.UserName, validatorContext.CaptchaResponse.Score); - } - var valid = await ValidateContextAsync(context, validatorContext); var user = validatorContext.User; - if (!valid) - { - await UpdateFailedAuthDetailsAsync(user, false, !validatorContext.KnownDevice); - } - if (!valid || isBot) { + if (isBot) + { + _logger.LogInformation(Constants.BypassFiltersEventId, + "Login attempt for {UserName} detected as a captcha bot with score {CaptchaScore}.", + request.UserName, validatorContext.CaptchaResponse.Score); + } + + if (!valid) + { + await UpdateFailedAuthDetailsAsync(user, false, !validatorContext.KnownDevice); + } + await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user); return; } - var (isTwoFactorRequired, twoFactorOrganization) = await _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(user, request); - var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString(); - var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString(); - var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1"; - var validTwoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) && - !string.IsNullOrWhiteSpace(twoFactorProvider); - - if (isTwoFactorRequired) + // 2. Does this user belong to an organization that requires SSO + validatorContext.SsoRequired = await RequireSsoLoginAsync(user, request.GrantType); + if (validatorContext.SsoRequired) { - // 2FA required and not provided response + SetSsoResult(context, + new Dictionary + { + { "ErrorModel", new ErrorResponseModel("SSO authentication is required.") } + }); + return; + } + + // 3. Check if 2FA is required + (validatorContext.TwoFactorRequired, var twoFactorOrganization) = await _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(user, request); + // This flag is used to determine if the user wants a rememberMe token sent when authentication is successful + var returnRememberMeToken = false; + if (validatorContext.TwoFactorRequired) + { + var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString(); + var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString(); + var validTwoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) && + !string.IsNullOrWhiteSpace(twoFactorProvider); + // response for 2FA required and not provided state if (!validTwoFactorRequest || !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType)) { @@ -125,18 +139,14 @@ public abstract class BaseRequestValidator where T : class return; } - var verified = await _twoFactorAuthenticationValidator + var twoFactorTokenValid = await _twoFactorAuthenticationValidator .VerifyTwoFactor(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken); - // 2FA required but request not valid or remember token expired response - if (!verified || isBot) + // response for 2FA required but request is not valid or remember token expired state + if (!twoFactorTokenValid) { - if (twoFactorProviderType != TwoFactorProviderType.Remember) - { - await UpdateFailedAuthDetailsAsync(user, true, !validatorContext.KnownDevice); - await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user); - } - else if (twoFactorProviderType == TwoFactorProviderType.Remember) + // The remember me token has expired + if (twoFactorProviderType == TwoFactorProviderType.Remember) { var resultDict = await _twoFactorAuthenticationValidator .BuildTwoFactorResultAsync(user, twoFactorOrganization); @@ -145,16 +155,34 @@ public abstract class BaseRequestValidator where T : class resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user)); SetTwoFactorResult(context, resultDict); } + else + { + await UpdateFailedAuthDetailsAsync(user, true, !validatorContext.KnownDevice); + await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user); + } return; } - } - else - { - validTwoFactorRequest = false; - twoFactorRemember = false; + + // When the two factor authentication is successful, we can check if the user wants a rememberMe token + var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1"; + if (twoFactorRemember // Check if the user wants a rememberMe token + && twoFactorTokenValid // Make sure two factor authentication was successful + && twoFactorProviderType != TwoFactorProviderType.Remember) // if the two factor auth was rememberMe do not send another token + { + returnRememberMeToken = true; + } } - // Force legacy users to the web for migration + // 4. Check if the user is logging in from a new device + var deviceValid = await _deviceValidator.ValidateRequestDeviceAsync(request, validatorContext); + if (!deviceValid) + { + SetValidationErrorResult(context, validatorContext); + await LogFailedLoginEvent(validatorContext.User, EventType.User_FailedLogIn); + return; + } + + // 5. Force legacy users to the web for migration if (FeatureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers)) { if (UserService.IsLegacyUser(user) && request.ClientId != "web") @@ -164,24 +192,7 @@ public abstract class BaseRequestValidator where T : class } } - if (await IsValidAuthTypeAsync(user, request.GrantType)) - { - var device = await _deviceValidator.SaveDeviceAsync(user, request); - if (device == null) - { - await BuildErrorResultAsync("No device information provided.", false, context, user); - return; - } - await BuildSuccessResultAsync(user, context, device, validTwoFactorRequest && twoFactorRemember); - } - else - { - SetSsoResult(context, - new Dictionary - { - { "ErrorModel", new ErrorResponseModel("SSO authentication is required.") } - }); - } + await BuildSuccessResultAsync(user, context, validatorContext.Device, returnRememberMeToken); } protected async Task FailAuthForLegacyUserAsync(User user, T context) @@ -235,6 +246,17 @@ public abstract class BaseRequestValidator where T : class await SetSuccessResult(context, user, claims, customResponse); } + /// + /// This does two things, it sets the error result for the current ValidatorContext _and_ it logs error. + /// These two things should be seperated to maintain single concerns. + /// + /// Error message for the error result + /// bool that controls how the error is logged + /// used to set the error result in the current validator + /// used to associate the failed login with a user + /// void + [Obsolete("Consider using SetValidationErrorResult to set the validation result, and LogFailedLoginEvent " + + "to log the failure.")] protected async Task BuildErrorResultAsync(string message, bool twoFactorRequest, T context, User user) { if (user != null) @@ -255,41 +277,80 @@ public abstract class BaseRequestValidator where T : class new Dictionary { { "ErrorModel", new ErrorResponseModel(message) } }); } + protected async Task LogFailedLoginEvent(User user, EventType eventType) + { + if (user != null) + { + await _eventService.LogUserEventAsync(user.Id, eventType); + } + + if (_globalSettings.SelfHosted) + { + string formattedMessage; + switch (eventType) + { + case EventType.User_FailedLogIn: + formattedMessage = string.Format("Failed login attempt. {0}", $" {CurrentContext.IpAddress}"); + break; + case EventType.User_FailedLogIn2fa: + formattedMessage = string.Format("Failed login attempt, 2FA invalid.{0}", $" {CurrentContext.IpAddress}"); + break; + default: + formattedMessage = "Failed login attempt."; + break; + } + _logger.LogWarning(Constants.BypassFiltersEventId, formattedMessage); + } + await Task.Delay(2000); // Delay for brute force. + } + + [Obsolete("Consider using SetValidationErrorResult instead.")] protected abstract void SetTwoFactorResult(T context, Dictionary customResponse); - + [Obsolete("Consider using SetValidationErrorResult instead.")] protected abstract void SetSsoResult(T context, Dictionary customResponse); + [Obsolete("Consider using SetValidationErrorResult instead.")] + protected abstract void SetErrorResult(T context, Dictionary customResponse); + /// + /// This consumes the ValidationErrorResult property in the CustomValidatorRequestContext and sets + /// it appropriately in the response object for the token and grant validators. + /// + /// The current grant or token context + /// The modified request context containing material used to build the response object + protected abstract void SetValidationErrorResult(T context, CustomValidatorRequestContext requestContext); protected abstract Task SetSuccessResult(T context, User user, List claims, Dictionary customResponse); - protected abstract void SetErrorResult(T context, Dictionary customResponse); protected abstract ClaimsPrincipal GetSubject(T context); /// /// Check if the user is required to authenticate via SSO. If the user requires SSO, but they are /// logging in using an API Key (client_credentials) then they are allowed to bypass the SSO requirement. + /// If the GrantType is authorization_code or client_credentials we know the user is trying to login + /// using the SSO flow so they are allowed to continue. /// /// user trying to login /// magic string identifying the grant type requested - /// - private async Task IsValidAuthTypeAsync(User user, string grantType) + /// true if sso required; false if not required or already in process + private async Task RequireSsoLoginAsync(User user, string grantType) { if (grantType == "authorization_code" || grantType == "client_credentials") { - // Already using SSO to authorize, finish successfully - // Or login via api key, skip SSO requirement - return true; - } - - // Check if user belongs to any organization with an active SSO policy - var anySsoPoliciesApplicableToUser = await PolicyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed); - if (anySsoPoliciesApplicableToUser) - { + // Already using SSO to authenticate, or logging-in via api key to skip SSO requirement + // allow to authenticate successfully return false; } - // Default - continue validation process - return true; + // Check if user belongs to any organization with an active SSO policy + var anySsoPoliciesApplicableToUser = await PolicyService.AnyPoliciesApplicableToUserAsync( + user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed); + if (anySsoPoliciesApplicableToUser) + { + return true; + } + + // Default - SSO is not required + return false; } private async Task ResetFailedAuthDetailsAsync(User user) @@ -350,7 +411,7 @@ public abstract class BaseRequestValidator where T : class var orgs = (await CurrentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)) .ToList(); - if (!orgs.Any()) + if (orgs.Count == 0) { return null; } diff --git a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index c826243f88..fb7b129b09 100644 --- a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs @@ -89,8 +89,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator ValidateContextAsync(CustomTokenRequestValidationContext context, @@ -162,6 +161,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator customResponse) { @@ -172,16 +172,18 @@ public class CustomTokenRequestValidator : BaseRequestValidator customResponse) { Debug.Assert(context.Result is not null); context.Result.Error = "invalid_grant"; - context.Result.ErrorDescription = "Single Sign on required."; + context.Result.ErrorDescription = "Sso authentication required."; context.Result.IsError = true; context.Result.CustomResponse = customResponse; } + [Obsolete("Consider using SetGrantValidationErrorResult instead.")] protected override void SetErrorResult(CustomTokenRequestValidationContext context, Dictionary customResponse) { @@ -190,4 +192,14 @@ public class CustomTokenRequestValidator : BaseRequestValidator - /// Save a device to the database. If the device is already known, it will be returned. - ///
- /// The user is assumed NOT null, still going to check though - /// Duende Validated Request that contains the data to create the device object - /// Returns null if user or device is malformed; The existing device if already in DB; a new device login - Task SaveDeviceAsync(User user, ValidatedTokenRequest request); - /// - /// Check if a device is known to the user. - /// - /// current user trying to authenticate - /// contains raw information that is parsed about the device - /// true if the device is known, false if it is not - Task KnownDeviceAsync(User user, ValidatedTokenRequest request); -} - public class DeviceValidator( IDeviceService deviceService, IDeviceRepository deviceRepository, GlobalSettings globalSettings, IMailService mailService, - ICurrentContext currentContext) : IDeviceValidator + ICurrentContext currentContext, + IUserService userService, + IFeatureService featureService) : IDeviceValidator { private readonly IDeviceService _deviceService = deviceService; private readonly IDeviceRepository _deviceRepository = deviceRepository; private readonly GlobalSettings _globalSettings = globalSettings; private readonly IMailService _mailService = mailService; private readonly ICurrentContext _currentContext = currentContext; + private readonly IUserService _userService = userService; + private readonly IFeatureService _featureService = featureService; - /// - /// Save a device to the database. If the device is already known, it will be returned. - /// - /// The user is assumed NOT null, still going to check though - /// Duende Validated Request that contains the data to create the device object - /// Returns null if user or device is malformed; The existing device if already in DB; a new device login - public async Task SaveDeviceAsync(User user, ValidatedTokenRequest request) + public async Task ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context) { - var device = GetDeviceFromRequest(request); - if (device != null && user != null) + // Parse device from request and return early if no device information is provided + var requestDevice = context.Device ?? GetDeviceFromRequest(request); + // If context.Device and request device information are null then return error + // backwards compatibility -- check if user is null + // PM-13340: Null user check happens in the HandleNewDeviceVerificationAsync method and can be removed from here + if (requestDevice == null || context.User == null) { - var existingDevice = await GetKnownDeviceAsync(user, device); - if (existingDevice == null) - { - device.UserId = user.Id; - await _deviceService.SaveAsync(device); - - // This makes sure the user isn't sent a "new device" email on their first login - var now = DateTime.UtcNow; - if (now - user.CreationDate > TimeSpan.FromMinutes(10)) - { - var deviceType = device.Type.GetType().GetMember(device.Type.ToString()) - .FirstOrDefault()?.GetCustomAttribute()?.GetName(); - if (!_globalSettings.DisableEmailNewDevice) - { - await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now, - _currentContext.IpAddress); - } - } - return device; - } - return existingDevice; + (context.ValidationErrorResult, context.CustomResponse) = + BuildDeviceErrorResult(DeviceValidationResultType.NoDeviceInformationProvided); + return false; } - return null; + + // if not a new device request then check if the device is known + if (!NewDeviceOtpRequest(request)) + { + var knownDevice = await GetKnownDeviceAsync(context.User, requestDevice); + // if the device is know then we return the device fetched from the database + // returning the database device is important for TDE + if (knownDevice != null) + { + context.KnownDevice = true; + context.Device = knownDevice; + return true; + } + } + + // We have established that the device is unknown at this point; begin new device verification + // PM-13340: remove feature flag + if (_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) && + request.GrantType == "password" && + request.Raw["AuthRequest"] == null && + !context.TwoFactorRequired && + !context.SsoRequired && + _globalSettings.EnableNewDeviceVerification) + { + // We only want to return early if the device is invalid or there is an error + var validationResult = await HandleNewDeviceVerificationAsync(context.User, request); + if (validationResult != DeviceValidationResultType.Success) + { + (context.ValidationErrorResult, context.CustomResponse) = + BuildDeviceErrorResult(validationResult); + if (validationResult == DeviceValidationResultType.NewDeviceVerificationRequired) + { + await _userService.SendOTPAsync(context.User); + } + return false; + } + } + + // At this point we have established either new device verification is not required or the NewDeviceOtp is valid + requestDevice.UserId = context.User.Id; + await _deviceService.SaveAsync(requestDevice); + context.Device = requestDevice; + + // backwards compatibility -- If NewDeviceVerification not enabled send the new login emails + // PM-13340: removal Task; remove entire if block emails should no longer be sent + if (!_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)) + { + // This ensures the user doesn't receive a "new device" email on the first login + var now = DateTime.UtcNow; + if (now - context.User.CreationDate > TimeSpan.FromMinutes(10)) + { + var deviceType = requestDevice.Type.GetType().GetMember(requestDevice.Type.ToString()) + .FirstOrDefault()?.GetCustomAttribute()?.GetName(); + if (!_globalSettings.DisableEmailNewDevice) + { + await _mailService.SendNewDeviceLoggedInEmail(context.User.Email, deviceType, now, + _currentContext.IpAddress); + } + } + } + return true; } - public async Task KnownDeviceAsync(User user, ValidatedTokenRequest request) => - (await GetKnownDeviceAsync(user, GetDeviceFromRequest(request))) != default; + /// + /// Checks the if the requesting deice requires new device verification otherwise saves the device to the database + /// + /// user attempting to authenticate + /// The Request is used to check for the NewDeviceOtp and for the raw device data + /// returns deviceValtaionResultType + private async Task HandleNewDeviceVerificationAsync(User user, ValidatedRequest request) + { + // currently unreachable due to backward compatibility + // PM-13340: will address this + if (user == null) + { + return DeviceValidationResultType.InvalidUser; + } - private async Task GetKnownDeviceAsync(User user, Device device) + // parse request for NewDeviceOtp to validate + var newDeviceOtp = request.Raw["NewDeviceOtp"]?.ToString(); + // we only check null here since an empty OTP will be considered an incorrect OTP + if (newDeviceOtp != null) + { + // verify the NewDeviceOtp + var otpValid = await _userService.VerifyOTPAsync(user, newDeviceOtp); + if (otpValid) + { + return DeviceValidationResultType.Success; + } + return DeviceValidationResultType.InvalidNewDeviceOtp; + } + + // if a user has no devices they are assumed to be newly registered user which does not require new device verification + var devices = await _deviceRepository.GetManyByUserIdAsync(user.Id); + if (devices.Count == 0) + { + return DeviceValidationResultType.Success; + } + + // if we get to here then we need to send a new device verification email + return DeviceValidationResultType.NewDeviceVerificationRequired; + } + + public async Task GetKnownDeviceAsync(User user, Device device) { if (user == null || device == null) { - return default; + return null; } + return await _deviceRepository.GetByIdentifierAsync(device.Identifier, user.Id); } - private static Device GetDeviceFromRequest(ValidatedRequest request) + public static Device GetDeviceFromRequest(ValidatedRequest request) { var deviceIdentifier = request.Raw["DeviceIdentifier"]?.ToString(); var requestDeviceType = request.Raw["DeviceType"]?.ToString(); @@ -112,4 +179,49 @@ public class DeviceValidator( PushToken = string.IsNullOrWhiteSpace(devicePushToken) ? null : devicePushToken }; } + + /// + /// Checks request for the NewDeviceOtp field to determine if a new device verification is required. + /// + /// + /// + public static bool NewDeviceOtpRequest(ValidatedRequest request) + { + return !string.IsNullOrEmpty(request.Raw["NewDeviceOtp"]?.ToString()); + } + + /// + /// This builds builds the error result for the various grant and token validators. The Success type is not used here. + /// + /// DeviceValidationResultType that is an error, success type is not used. + /// validation result used by grant and token validators, and the custom response for either Grant or Token response objects. + private static (Duende.IdentityServer.Validation.ValidationResult, Dictionary) BuildDeviceErrorResult(DeviceValidationResultType errorType) + { + var result = new Duende.IdentityServer.Validation.ValidationResult + { + IsError = true, + Error = "device_error", + }; + var customResponse = new Dictionary(); + switch (errorType) + { + case DeviceValidationResultType.InvalidUser: + result.ErrorDescription = "Invalid user"; + customResponse.Add("ErrorModel", new ErrorResponseModel("invalid user")); + break; + case DeviceValidationResultType.InvalidNewDeviceOtp: + result.ErrorDescription = "Invalid New Device OTP"; + customResponse.Add("ErrorModel", new ErrorResponseModel("invalid new device otp")); + break; + case DeviceValidationResultType.NewDeviceVerificationRequired: + result.ErrorDescription = "New device verification required"; + customResponse.Add("ErrorModel", new ErrorResponseModel("new device verification required")); + break; + case DeviceValidationResultType.NoDeviceInformationProvided: + result.ErrorDescription = "No device information provided"; + customResponse.Add("ErrorModel", new ErrorResponseModel("no device information provided")); + break; + } + return (result, customResponse); + } } diff --git a/src/Identity/IdentityServer/RequestValidators/IDeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/IDeviceValidator.cs new file mode 100644 index 0000000000..0bff7e4fab --- /dev/null +++ b/src/Identity/IdentityServer/RequestValidators/IDeviceValidator.cs @@ -0,0 +1,24 @@ +using Bit.Core.Entities; +using Duende.IdentityServer.Validation; + +namespace Bit.Identity.IdentityServer.RequestValidators; + +public interface IDeviceValidator +{ + /// + /// Fetches device from the database using the Device Identifier and the User Id to know if the user + /// has ever tried to authenticate with this specific instance of Bitwarden. + /// + /// user attempting to authenticate + /// current instance of Bitwarden the user is interacting with + /// null or Device + Task GetKnownDeviceAsync(User user, Device device); + + /// + /// Validate the requesting device. Modifies the ValidatorRequestContext with error result if any. + /// + /// The Request is used to check for the NewDeviceOtp and for the raw device data + /// Contains two factor and sso context that are important for decisions on new device verification + /// returns true if device is valid and no other action required; if false modifies the context with an error result to be returned; + Task ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context); +} diff --git a/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs b/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs index f072a64177..852bf27e40 100644 --- a/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs @@ -75,11 +75,16 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator customResponse) { @@ -163,6 +169,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator customResponse) { @@ -170,12 +177,25 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator customResponse) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, customResponse: customResponse); } + protected override void SetValidationErrorResult( + ResourceOwnerPasswordValidationContext context, CustomValidatorRequestContext requestContext) + { + context.Result = new GrantValidationResult + { + Error = requestContext.ValidationErrorResult.Error, + ErrorDescription = requestContext.ValidationErrorResult.ErrorDescription, + IsError = true, + CustomResponse = requestContext.CustomResponse + }; + } + protected override ClaimsPrincipal GetSubject(ResourceOwnerPasswordValidationContext context) { return context.Result.Subject; @@ -183,28 +203,26 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator ValidateContextAsync(ExtensionGrantValidationContext context, @@ -128,6 +122,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator customResponse) { @@ -135,6 +130,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator customResponse) { @@ -142,9 +138,21 @@ public class WebAuthnGrantValidator : BaseRequestValidator customResponse) + [Obsolete("Consider using SetValidationErrorResult instead.")] + protected override void SetErrorResult(ExtensionGrantValidationContext context, Dictionary customResponse) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, customResponse: customResponse); } + + protected override void SetValidationErrorResult( + ExtensionGrantValidationContext context, CustomValidatorRequestContext requestContext) + { + context.Result = new GrantValidationResult + { + Error = requestContext.ValidationErrorResult.Error, + ErrorDescription = requestContext.ValidationErrorResult.ErrorDescription, + IsError = true, + CustomResponse = requestContext.CustomResponse + }; + } } diff --git a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs index 703faed48c..4bec8d8167 100644 --- a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs @@ -5,14 +5,11 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Identity.IdentityServer.RequestValidators; using Bit.Identity.Models.Request.Accounts; using Bit.IntegrationTestCommon.Factories; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; -using Duende.IdentityServer.Validation; using Microsoft.AspNetCore.Identity; -using NSubstitute; using Xunit; namespace Bit.Identity.IntegrationTest.RequestValidation; @@ -217,48 +214,6 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture(sub => - { - sub.SaveDeviceAsync(Arg.Any(), Arg.Any()) - .Returns(null as Device); - }); - - // Add User - await factory.RegisterAsync(new RegisterRequestModel - { - Email = DefaultUsername, - MasterPasswordHash = DefaultPassword - }); - var userManager = factory.GetService>(); - await factory.RegisterAsync(new RegisterRequestModel - { - Email = DefaultUsername, - MasterPasswordHash = DefaultPassword - }); - var user = await userManager.FindByEmailAsync(DefaultUsername); - Assert.NotNull(user); - - // Act - var context = await factory.Server.PostAsync("/connect/token", - GetFormUrlEncodedContent(), - context => context.SetAuthEmail(DefaultUsername)); - - // Assert - var body = await AssertHelper.AssertResponseTypeIs(context); - var root = body.RootElement; - - var errorModel = AssertHelper.AssertJsonProperty(root, "ErrorModel", JsonValueKind.Object); - var errorMessage = AssertHelper.AssertJsonProperty(errorModel, "Message", JsonValueKind.String).GetString(); - Assert.Equal("No device information provided.", errorMessage); - } - private async Task EnsureUserCreatedAsync(IdentityApplicationFactory factory = null) { factory ??= _factory; @@ -290,6 +245,18 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture + { + { "scope", "api offline_access" }, + { "client_id", "web" }, + { "grant_type", "password" }, + { "username", DefaultUsername }, + { "password", DefaultPassword }, + }); + } + private static string DeviceTypeAsString(DeviceType deviceType) { return ((int)deviceType).ToString(); diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index d0372202ad..02b6982419 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -22,7 +22,6 @@ using NSubstitute; using Xunit; using AuthFixtures = Bit.Identity.Test.AutoFixture; - namespace Bit.Identity.Test.IdentityServer; public class BaseRequestValidatorTests @@ -82,10 +81,10 @@ public class BaseRequestValidatorTests } /* Logic path - ValidateAsync -> _Logger.LogInformation - |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync - |-> SetErrorResult - */ + * ValidateAsync -> _Logger.LogInformation + * |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync + * |-> SetErrorResult + */ [Theory, BitAutoData] public async Task ValidateAsync_IsBot_UserNotNull_ShouldBuildErrorResult_ShouldLogFailedLoginEvent( [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, @@ -112,11 +111,11 @@ public class BaseRequestValidatorTests } /* Logic path - ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync - |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync - (self hosted) |-> _logger.LogWarning() - |-> SetErrorResult - */ + * ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync + * |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync + * (self hosted) |-> _logger.LogWarning() + * |-> SetErrorResult + */ [Theory, BitAutoData] public async Task ValidateAsync_ContextNotValid_SelfHosted_ShouldBuildErrorResult_ShouldLogWarning( [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, @@ -140,10 +139,10 @@ public class BaseRequestValidatorTests } /* Logic path - ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync - |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync - |-> SetErrorResult - */ + * ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync + * |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync + * |-> SetErrorResult + */ [Theory, BitAutoData] public async Task ValidateAsync_ContextNotValid_MaxAttemptLogin_ShouldSendEmail( [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, @@ -177,134 +176,97 @@ public class BaseRequestValidatorTests Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message); } - - /* Logic path - ValidateAsync -> IsValidAuthTypeAsync -> SaveDeviceAsync -> BuildErrorResult - */ [Theory, BitAutoData] - public async Task ValidateAsync_AuthCodeGrantType_DeviceNull_ShouldError( + public async Task ValidateAsync_DeviceNotValidated_ShouldLogError( [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, CustomValidatorRequestContext requestContext, GrantValidationResult grantResult) { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); - _twoFactorAuthenticationValidator - .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new Tuple(false, default))); - + // 1 -> to pass context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; - context.ValidatedTokenRequest.GrantType = "authorization_code"; + // 2 -> will result to false with no extra configuration + // 3 -> set two factor to be false + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), tokenRequest) + .Returns(Task.FromResult(new Tuple(false, null))); + + // 4 -> set up device validator to fail + requestContext.KnownDevice = false; + tokenRequest.GrantType = "password"; + _deviceValidator.ValidateRequestDeviceAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(false)); + + // 5 -> not legacy user + _userService.IsLegacyUser(Arg.Any()) + .Returns(false); // Act await _sut.ValidateAsync(context); - var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"]; - // Assert Assert.True(context.GrantResult.IsError); - Assert.Equal("No device information provided.", errorResponse.Message); + await _eventService.Received(1) + .LogUserEventAsync(context.CustomValidatorRequestContext.User.Id, EventType.User_FailedLogIn); } - /* Logic path - ValidateAsync -> IsValidAuthTypeAsync -> SaveDeviceAsync -> BuildSuccessResultAsync - */ [Theory, BitAutoData] - public async Task ValidateAsync_ClientCredentialsGrantType_ShouldSucceed( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, - CustomValidatorRequestContext requestContext, - GrantValidationResult grantResult, - Device device) - { - // Arrange - var context = CreateContext(tokenRequest, requestContext, grantResult); - _twoFactorAuthenticationValidator - .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new Tuple(false, null))); - - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; - _sut.isValid = true; - - context.CustomValidatorRequestContext.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(1); - _globalSettings.DisableEmailNewDevice = false; - - context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device - - _deviceValidator.SaveDeviceAsync(Arg.Any(), Arg.Any()) - .Returns(device); - // Act - await _sut.ValidateAsync(context); - - // Assert - Assert.False(context.GrantResult.IsError); - } - - /* Logic path - ValidateAsync -> IsValidAuthTypeAsync -> SaveDeviceAsync -> BuildSuccessResultAsync - */ - [Theory, BitAutoData] - public async Task ValidateAsync_ClientCredentialsGrantType_ExistingDevice_ShouldSucceed( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, - CustomValidatorRequestContext requestContext, - GrantValidationResult grantResult, - Device device) - { - // Arrange - var context = CreateContext(tokenRequest, requestContext, grantResult); - - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; - _sut.isValid = true; - - context.CustomValidatorRequestContext.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(1); - _globalSettings.DisableEmailNewDevice = false; - - context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device - - _deviceValidator.SaveDeviceAsync(Arg.Any(), Arg.Any()) - .Returns(device); - _twoFactorAuthenticationValidator - .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new Tuple(false, null))); - // Act - await _sut.ValidateAsync(context); - - // Assert - await _eventService.LogUserEventAsync( - context.CustomValidatorRequestContext.User.Id, EventType.User_LoggedIn); - await _userRepository.Received(1).ReplaceAsync(Arg.Any()); - - Assert.False(context.GrantResult.IsError); - } - - /* Logic path - ValidateAsync -> IsLegacyUser -> BuildErrorResultAsync - */ - [Theory, BitAutoData] - public async Task ValidateAsync_InvalidAuthType_ShouldSetSsoResult( + public async Task ValidateAsync_DeviceValidated_ShouldSucceed( [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, CustomValidatorRequestContext requestContext, GrantValidationResult grantResult) { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); - - context.ValidatedTokenRequest.Raw["DeviceIdentifier"] = "DeviceIdentifier"; - context.ValidatedTokenRequest.Raw["DevicePushToken"] = "DevicePushToken"; - context.ValidatedTokenRequest.Raw["DeviceName"] = "DeviceName"; - context.ValidatedTokenRequest.Raw["DeviceType"] = "Android"; // This needs to be an actual Type + // 1 -> to pass context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; - context.ValidatedTokenRequest.GrantType = ""; + // 2 -> will result to false with no extra configuration + // 3 -> set two factor to be false + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), tokenRequest) + .Returns(Task.FromResult(new Tuple(false, null))); + // 4 -> set up device validator to pass + _deviceValidator.ValidateRequestDeviceAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); + + // 5 -> not legacy user + _userService.IsLegacyUser(Arg.Any()) + .Returns(false); + + // Act + await _sut.ValidateAsync(context); + + // Assert + Assert.False(context.GrantResult.IsError); + } + + // Test grantTypes that require SSO when a user is in an organization that requires it + [Theory] + [BitAutoData("password")] + [BitAutoData("webauthn")] + [BitAutoData("refresh_token")] + public async Task ValidateAsync_GrantTypes_OrgSsoRequiredTrue_ShouldSetSsoResult( + string grantType, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + CustomValidatorRequestContext requestContext, + GrantValidationResult grantResult) + { + // Arrange + var context = CreateContext(tokenRequest, requestContext, grantResult); + context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; + _sut.isValid = true; + + context.ValidatedTokenRequest.GrantType = grantType; _policyService.AnyPoliciesApplicableToUserAsync( Arg.Any(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed) .Returns(Task.FromResult(true)); - _twoFactorAuthenticationValidator - .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new Tuple(false, null))); + // Act await _sut.ValidateAsync(context); @@ -314,6 +276,85 @@ public class BaseRequestValidatorTests Assert.Equal("SSO authentication is required.", errorResponse.Message); } + // Test grantTypes where SSO would be required but the user is not in an + // organization that requires it + [Theory] + [BitAutoData("password")] + [BitAutoData("webauthn")] + [BitAutoData("refresh_token")] + public async Task ValidateAsync_GrantTypes_OrgSsoRequiredFalse_ShouldSucceed( + string grantType, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + CustomValidatorRequestContext requestContext, + GrantValidationResult grantResult) + { + // Arrange + var context = CreateContext(tokenRequest, requestContext, grantResult); + context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; + _sut.isValid = true; + + context.ValidatedTokenRequest.GrantType = grantType; + + _policyService.AnyPoliciesApplicableToUserAsync( + Arg.Any(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed) + .Returns(Task.FromResult(false)); + _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest) + .Returns(Task.FromResult(new Tuple(false, null))); + _deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext) + .Returns(Task.FromResult(true)); + context.ValidatedTokenRequest.ClientId = "web"; + + // Act + await _sut.ValidateAsync(context); + + // Assert + await _eventService.Received(1).LogUserEventAsync( + context.CustomValidatorRequestContext.User.Id, EventType.User_LoggedIn); + await _userRepository.Received(1).ReplaceAsync(Arg.Any()); + + Assert.False(context.GrantResult.IsError); + + } + + // Test the grantTypes where SSO is in progress or not relevant + [Theory] + [BitAutoData("authorization_code")] + [BitAutoData("client_credentials")] + public async Task ValidateAsync_GrantTypes_SsoRequiredFalse_ShouldSucceed( + string grantType, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + CustomValidatorRequestContext requestContext, + GrantValidationResult grantResult) + { + // Arrange + var context = CreateContext(tokenRequest, requestContext, grantResult); + context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; + _sut.isValid = true; + + context.ValidatedTokenRequest.GrantType = grantType; + + _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest) + .Returns(Task.FromResult(new Tuple(false, null))); + _deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext) + .Returns(Task.FromResult(true)); + context.ValidatedTokenRequest.ClientId = "web"; + + // Act + await _sut.ValidateAsync(context); + + // Assert + await _policyService.DidNotReceive().AnyPoliciesApplicableToUserAsync( + Arg.Any(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed); + await _eventService.Received(1).LogUserEventAsync( + context.CustomValidatorRequestContext.User.Id, EventType.User_LoggedIn); + await _userRepository.Received(1).ReplaceAsync(Arg.Any()); + + Assert.False(context.GrantResult.IsError); + } + + /* Logic Path + * ValidateAsync -> UserService.IsLegacyUser -> FailAuthForLegacyUserAsync + */ [Theory, BitAutoData] public async Task ValidateAsync_IsLegacyUser_FailAuthForLegacyUserAsync( [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, @@ -332,6 +373,8 @@ public class BaseRequestValidatorTests _twoFactorAuthenticationValidator .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new Tuple(false, null))); + _deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext) + .Returns(Task.FromResult(true)); // Act await _sut.ValidateAsync(context); @@ -339,8 +382,9 @@ public class BaseRequestValidatorTests // Assert Assert.True(context.GrantResult.IsError); var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"]; - Assert.Equal($"Encryption key migration is required. Please log in to the web vault at {_globalSettings.BaseServiceUri.VaultWithHash}" - , errorResponse.Message); + var expectedMessage = $"Encryption key migration is required. Please log in to the web " + + $"vault at {_globalSettings.BaseServiceUri.VaultWithHash}"; + Assert.Equal(expectedMessage, errorResponse.Message); } private BaseRequestValidationContextFake CreateContext( @@ -367,4 +411,12 @@ public class BaseRequestValidatorTests Substitute.For(), Substitute.For>>()); } + + private void AddValidDeviceToRequest(ValidatedTokenRequest request) + { + request.Raw["DeviceIdentifier"] = "DeviceIdentifier"; + request.Raw["DeviceType"] = "Android"; // must be valid device type + request.Raw["DeviceName"] = "DeviceName"; + request.Raw["DevicePushToken"] = "DevicePushToken"; + } } diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index 2db792c936..304715b68c 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -1,9 +1,12 @@ -using Bit.Core.Context; +using Bit.Core; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Models.Api; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; +using Bit.Identity.IdentityServer; using Bit.Identity.IdentityServer.RequestValidators; using Bit.Test.Common.AutoFixture.Attributes; using Duende.IdentityServer.Validation; @@ -20,6 +23,8 @@ public class DeviceValidatorTests private readonly GlobalSettings _globalSettings; private readonly IMailService _mailService; private readonly ICurrentContext _currentContext; + private readonly IUserService _userService; + private readonly IFeatureService _featureService; private readonly DeviceValidator _sut; public DeviceValidatorTests() @@ -29,219 +34,550 @@ public class DeviceValidatorTests _globalSettings = new GlobalSettings(); _mailService = Substitute.For(); _currentContext = Substitute.For(); + _userService = Substitute.For(); + _featureService = Substitute.For(); _sut = new DeviceValidator( _deviceService, _deviceRepository, _globalSettings, _mailService, - _currentContext); + _currentContext, + _userService, + _featureService); } - [Theory] - [BitAutoData] - public async void SaveDeviceAsync_DeviceNull_ShouldReturnNull( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, - User user) - { - // Arrange - request.Raw["DeviceIdentifier"] = null; - - // Act - var device = await _sut.SaveDeviceAsync(user, request); - - // Assert - Assert.Null(device); - await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory] - [BitAutoData] - public async void SaveDeviceAsync_UserIsNull_ShouldReturnNull( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) - { - // Arrange - request = AddValidDeviceToRequest(request); - - // Act - var device = await _sut.SaveDeviceAsync(null, request); - - // Assert - Assert.Null(device); - await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory] - [BitAutoData] - public async void SaveDeviceAsync_ExistingUser_NewDevice_ReturnsDevice_SendsEmail( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, - User user) - { - // Arrange - request = AddValidDeviceToRequest(request); - - user.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11); - _globalSettings.DisableEmailNewDevice = false; - - // Act - var device = await _sut.SaveDeviceAsync(user, request); - - // Assert - Assert.NotNull(device); - Assert.Equal(user.Id, device.UserId); - Assert.Equal("DeviceIdentifier", device.Identifier); - Assert.Equal(DeviceType.Android, device.Type); - await _mailService.Received(1).SendNewDeviceLoggedInEmail( - user.Email, "Android", Arg.Any(), Arg.Any()); - } - - [Theory] - [BitAutoData] - public async void SaveDeviceAsync_ExistingUser_NewDevice_ReturnsDevice_SendEmailFalse( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, - User user) - { - // Arrange - request = AddValidDeviceToRequest(request); - - user.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11); - _globalSettings.DisableEmailNewDevice = true; - - // Act - var device = await _sut.SaveDeviceAsync(user, request); - - // Assert - Assert.NotNull(device); - Assert.Equal(user.Id, device.UserId); - Assert.Equal("DeviceIdentifier", device.Identifier); - Assert.Equal(DeviceType.Android, device.Type); - await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail( - user.Email, "Android", Arg.Any(), Arg.Any()); - } - - [Theory] - [BitAutoData] - public async void SaveDeviceAsync_DeviceIsKnown_ShouldReturnDevice( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, - User user, + [Theory, BitAutoData] + public async void GetKnownDeviceAsync_UserNull_ReturnsFalse( Device device) { // Arrange - request = AddValidDeviceToRequest(request); - - device.UserId = user.Id; - device.Identifier = "DeviceIdentifier"; - device.Type = DeviceType.Android; - device.Name = "DeviceName"; - device.PushToken = "DevicePushToken"; - _deviceRepository.GetByIdentifierAsync(device.Identifier, user.Id).Returns(device); + // AutoData arrages // Act - var resultDevice = await _sut.SaveDeviceAsync(user, request); + var result = await _sut.GetKnownDeviceAsync(null, device); // Assert - Assert.Equal(device, resultDevice); - await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + Assert.Null(result); } - [Theory] - [BitAutoData] - public async void SaveDeviceAsync_NewUser_DeviceUnknown_ShouldSaveDevice_NoEmail( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, - User user) - { - // Arrange - request = AddValidDeviceToRequest(request); - user.CreationDate = DateTime.UtcNow; - _deviceRepository.GetByIdentifierAsync(Arg.Any(), Arg.Any()).Returns(null as Device); - - // Act - var device = await _sut.SaveDeviceAsync(user, request); - - // Assert - Assert.NotNull(device); - Assert.Equal(user.Id, device.UserId); - Assert.Equal("DeviceIdentifier", device.Identifier); - Assert.Equal(DeviceType.Android, device.Type); - await _deviceService.Received(1).SaveAsync(device); - await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory] - [BitAutoData] - public async void KnownDeviceAsync_UserNull_ReturnsFalse( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) - { - // Arrange - request = AddValidDeviceToRequest(request); - - // Act - var result = await _sut.KnownDeviceAsync(null, request); - - // Assert - Assert.False(result); - } - - [Theory] - [BitAutoData] - public async void KnownDeviceAsync_DeviceNull_ReturnsFalse( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + [Theory, BitAutoData] + public async void GetKnownDeviceAsync_DeviceNull_ReturnsFalse( User user) { // Arrange // Device raw data is null which will cause the device to be null // Act - var result = await _sut.KnownDeviceAsync(user, request); + var result = await _sut.GetKnownDeviceAsync(user, null); // Assert - Assert.False(result); + Assert.Null(result); } - [Theory] - [BitAutoData] - public async void KnownDeviceAsync_DeviceNotInDatabase_ReturnsFalse( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, - User user) + [Theory, BitAutoData] + public async void GetKnownDeviceAsync_DeviceNotInDatabase_ReturnsFalse( + User user, + Device device) { // Arrange - request = AddValidDeviceToRequest(request); _deviceRepository.GetByIdentifierAsync(Arg.Any(), Arg.Any()) .Returns(null as Device); // Act - var result = await _sut.KnownDeviceAsync(user, request); + var result = await _sut.GetKnownDeviceAsync(user, device); // Assert - Assert.False(result); + Assert.Null(result); } - [Theory] - [BitAutoData] - public async void KnownDeviceAsync_UserAndDeviceValid_ReturnsTrue( + [Theory, BitAutoData] + public async void GetKnownDeviceAsync_UserAndDeviceValid_ReturnsTrue( [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, User user, Device device) { // Arrange - request = AddValidDeviceToRequest(request); + AddValidDeviceToRequest(request); _deviceRepository.GetByIdentifierAsync(Arg.Any(), Arg.Any()) .Returns(device); // Act - var result = await _sut.KnownDeviceAsync(user, request); + var result = await _sut.GetKnownDeviceAsync(user, device); + + // Assert + Assert.NotNull(result); + } + + [Theory] + [BitAutoData("not null", "Android", "")] + [BitAutoData("not null", "", "not null")] + [BitAutoData("", "Android", "not null")] + public void GetDeviceFromRequest_RawDeviceInfoNull_ReturnsNull( + string deviceIdentifier, + string deviceType, + string deviceName, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + request.Raw["DeviceIdentifier"] = deviceIdentifier; + request.Raw["DeviceType"] = deviceType; + request.Raw["DeviceName"] = deviceName; + + // Act + var result = DeviceValidator.GetDeviceFromRequest(request); + + // Assert + Assert.Null(result); + } + + [Theory, BitAutoData] + public void GetDeviceFromRequest_RawDeviceInfoValid_ReturnsDevice( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + AddValidDeviceToRequest(request); + + // Act + var result = DeviceValidator.GetDeviceFromRequest(request); + + // Assert + Assert.NotNull(result); + Assert.Equal("DeviceIdentifier", result.Identifier); + Assert.Equal("DeviceName", result.Name); + Assert.Equal(DeviceType.Android, result.Type); + Assert.Equal("DevicePushToken", result.PushToken); + } + + [Theory, BitAutoData] + public async void ValidateRequestDeviceAsync_DeviceNull_ContextModified_ReturnsFalse( + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + context.KnownDevice = false; + context.Device = null; + + // Act + Assert.NotNull(context.User); + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + await _deviceService.Received(0).SaveAsync(Arg.Any()); + + Assert.False(result); + Assert.NotNull(context.CustomResponse["ErrorModel"]); + var expectedErrorModel = new ErrorResponseModel("no device information provided"); + var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"]; + Assert.Equal(expectedErrorModel.Message, actualResponse.Message); + } + + [Theory, BitAutoData] + public async void ValidateRequestDeviceAsync_RequestDeviceKnown_ContextDeviceModified_ReturnsTrue( + Device device, + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + context.KnownDevice = false; + context.Device = null; + AddValidDeviceToRequest(request); + _deviceRepository.GetByIdentifierAsync(Arg.Any(), Arg.Any()) + .Returns(device); + + // Act + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + await _deviceService.Received(0).SaveAsync(Arg.Any()); + + Assert.True(result); + Assert.False(context.CustomResponse.ContainsKey("ErrorModel")); + Assert.NotNull(context.Device); + Assert.Equal(context.Device, device); + } + + [Theory, BitAutoData] + public async void ValidateRequestDeviceAsync_ContextDeviceKnown_ContextDeviceModified_ReturnsTrue( + Device databaseDevice, + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + context.KnownDevice = false; + _deviceRepository.GetByIdentifierAsync(Arg.Any(), Arg.Any()) + .Returns(databaseDevice); + // we want to show that the context device is updated when the device is known + Assert.NotEqual(context.Device, databaseDevice); + + // Act + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + await _deviceService.Received(0).SaveAsync(Arg.Any()); + + Assert.True(result); + Assert.False(context.CustomResponse.ContainsKey("ErrorModel")); + Assert.Equal(context.Device, databaseDevice); + } + + [Theory, BitAutoData] + public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_SendsEmail_ReturnsTrue( + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + context.KnownDevice = false; + AddValidDeviceToRequest(request); + _globalSettings.DisableEmailNewDevice = false; + _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) + .Returns(null as Device); + _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) + .Returns(false); + // set user creation to more than 10 minutes ago + context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11); + + // Act + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + await _deviceService.Received(1).SaveAsync(context.Device); + await _mailService.Received(1).SendNewDeviceLoggedInEmail( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + Assert.True(result); + } + + [Theory, BitAutoData] + public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_NewUser_DoesNotSendEmail_ReturnsTrue( + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + context.KnownDevice = false; + AddValidDeviceToRequest(request); + _globalSettings.DisableEmailNewDevice = false; + _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) + .Returns(null as Device); + _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) + .Returns(false); + // set user creation to less than 10 minutes ago + context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(9); + + // Act + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + await _deviceService.Received(1).SaveAsync(context.Device); + await _mailService.Received(0).SendNewDeviceLoggedInEmail( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + Assert.True(result); + } + + [Theory, BitAutoData] + public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_DisableEmailTrue_DoesNotSendEmail_ReturnsTrue( + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + context.KnownDevice = false; + AddValidDeviceToRequest(request); + _globalSettings.DisableEmailNewDevice = true; + _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) + .Returns(null as Device); + _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) + .Returns(false); + + // Act + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + await _deviceService.Received(1).SaveAsync(context.Device); + await _mailService.Received(0).SendNewDeviceLoggedInEmail( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + Assert.True(result); + } + + [Theory] + [BitAutoData("webauthn")] + [BitAutoData("refresh_token")] + [BitAutoData("authorization_code")] + [BitAutoData("client_credentials")] + public async void ValidateRequestDeviceAsync_GrantTypeNotPassword_SavesDevice_ReturnsTrue( + string grantType, + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + context.KnownDevice = false; + ArrangeForHandleNewDeviceVerificationTest(context, request); + AddValidDeviceToRequest(request); + _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) + .Returns(null as Device); + _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) + .Returns(true); + + request.GrantType = grantType; + + // Act + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + await _deviceService.Received(1).SaveAsync(context.Device); + Assert.True(result); + } + + [Theory, BitAutoData] + public async void ValidateRequestDeviceAsync_IsAuthRequest_SavesDevice_ReturnsTrue( + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + context.KnownDevice = false; + ArrangeForHandleNewDeviceVerificationTest(context, request); + AddValidDeviceToRequest(request); + _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) + .Returns(null as Device); + _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) + .Returns(true); + + request.Raw.Add("AuthRequest", "authRequest"); + + // Act + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + await _deviceService.Received(1).SaveAsync(context.Device); + Assert.True(result); + } + + [Theory, BitAutoData] + public async void ValidateRequestDeviceAsync_TwoFactorRequired_SavesDevice_ReturnsTrue( + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + context.KnownDevice = false; + ArrangeForHandleNewDeviceVerificationTest(context, request); + AddValidDeviceToRequest(request); + _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) + .Returns(null as Device); + _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) + .Returns(true); + + context.TwoFactorRequired = true; + + // Act + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + await _deviceService.Received(1).SaveAsync(context.Device); + Assert.True(result); + } + + [Theory, BitAutoData] + public async void ValidateRequestDeviceAsync_SsoRequired_SavesDevice_ReturnsTrue( + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + context.KnownDevice = false; + ArrangeForHandleNewDeviceVerificationTest(context, request); + AddValidDeviceToRequest(request); + _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) + .Returns(null as Device); + _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) + .Returns(true); + + context.SsoRequired = true; + + // Act + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + await _deviceService.Received(1).SaveAsync(context.Device); + Assert.True(result); + } + + [Theory, BitAutoData] + public async void HandleNewDeviceVerificationAsync_UserNull_ContextModified_ReturnsInvalidUser( + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + ArrangeForHandleNewDeviceVerificationTest(context, request); + _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); + _globalSettings.EnableNewDeviceVerification = true; + + context.User = null; + + // Act + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + await _deviceService.Received(0).SaveAsync(Arg.Any()); + + Assert.False(result); + Assert.NotNull(context.CustomResponse["ErrorModel"]); + // PM-13340: The error message should be "invalid user" instead of "no device information provided" + var expectedErrorMessage = "no device information provided"; + var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"]; + Assert.Equal(expectedErrorMessage, actualResponse.Message); + } + + [Theory, BitAutoData] + public async void HandleNewDeviceVerificationAsync_NewDeviceOtpValid_ReturnsSuccess( + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + ArrangeForHandleNewDeviceVerificationTest(context, request); + _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); + _globalSettings.EnableNewDeviceVerification = true; + + var newDeviceOtp = "123456"; + request.Raw.Add("NewDeviceOtp", newDeviceOtp); + + _userService.VerifyOTPAsync(context.User, newDeviceOtp).Returns(true); + + // Act + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + await _userService.Received(0).SendOTPAsync(context.User); + await _deviceService.Received(1).SaveAsync(context.Device); + + Assert.True(result); + Assert.False(context.CustomResponse.ContainsKey("ErrorModel")); + Assert.Equal(context.User.Id, context.Device.UserId); + Assert.NotNull(context.Device); + } + + [Theory] + [BitAutoData("")] + [BitAutoData("123456")] + public async void HandleNewDeviceVerificationAsync_NewDeviceOtpInvalid_ReturnsInvalidNewDeviceOtp( + string newDeviceOtp, + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + ArrangeForHandleNewDeviceVerificationTest(context, request); + _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); + _globalSettings.EnableNewDeviceVerification = true; + + request.Raw.Add("NewDeviceOtp", newDeviceOtp); + + _userService.VerifyOTPAsync(context.User, newDeviceOtp).Returns(false); + + // Act + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + await _userService.DidNotReceive().SendOTPAsync(Arg.Any()); + await _deviceService.Received(0).SaveAsync(Arg.Any()); + + Assert.False(result); + Assert.NotNull(context.CustomResponse["ErrorModel"]); + var expectedErrorMessage = "invalid new device otp"; + var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"]; + Assert.Equal(expectedErrorMessage, actualResponse.Message); + } + + [Theory, BitAutoData] + public async void HandleNewDeviceVerificationAsync_UserHasNoDevices_ReturnsSuccess( + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + ArrangeForHandleNewDeviceVerificationTest(context, request); + _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); + _globalSettings.EnableNewDeviceVerification = true; + _deviceRepository.GetManyByUserIdAsync(context.User.Id).Returns([]); + + // Act + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + await _userService.Received(0).VerifyOTPAsync(Arg.Any(), Arg.Any()); + await _userService.Received(0).SendOTPAsync(Arg.Any()); + await _deviceService.Received(1).SaveAsync(context.Device); + + Assert.True(result); + Assert.False(context.CustomResponse.ContainsKey("ErrorModel")); + Assert.Equal(context.User.Id, context.Device.UserId); + Assert.NotNull(context.Device); + } + + [Theory, BitAutoData] + public async void HandleNewDeviceVerificationAsync_NewDeviceOtpEmpty_UserHasDevices_ReturnsNewDeviceVerificationRequired( + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + ArrangeForHandleNewDeviceVerificationTest(context, request); + _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); + _globalSettings.EnableNewDeviceVerification = true; + _deviceRepository.GetManyByUserIdAsync(context.User.Id).Returns([new Device()]); + + // Act + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + await _userService.Received(1).SendOTPAsync(context.User); + await _deviceService.Received(0).SaveAsync(Arg.Any()); + + Assert.False(result); + Assert.NotNull(context.CustomResponse["ErrorModel"]); + var expectedErrorMessage = "new device verification required"; + var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"]; + Assert.Equal(expectedErrorMessage, actualResponse.Message); + } + + [Theory, BitAutoData] + public void NewDeviceOtpRequest_NewDeviceOtpNull_ReturnsFalse( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + // Autodata arranges + + // Act + var result = DeviceValidator.NewDeviceOtpRequest(request); + + // Assert + Assert.False(result); + } + + [Theory, BitAutoData] + public void NewDeviceOtpRequest_NewDeviceOtpNotNull_ReturnsTrue( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + request.Raw["NewDeviceOtp"] = "123456"; + + // Act + var result = DeviceValidator.NewDeviceOtpRequest(request); // Assert Assert.True(result); } - private ValidatedTokenRequest AddValidDeviceToRequest(ValidatedTokenRequest request) + private static void AddValidDeviceToRequest(ValidatedTokenRequest request) { request.Raw["DeviceIdentifier"] = "DeviceIdentifier"; - request.Raw["DeviceType"] = "Android"; + request.Raw["DeviceType"] = "Android"; // must be valid device type request.Raw["DeviceName"] = "DeviceName"; request.Raw["DevicePushToken"] = "DevicePushToken"; - return request; + } + + /// + /// Configures the request context to facilitate testing the HandleNewDeviceVerificationAsync method. + /// + /// test context + /// test request + private static void ArrangeForHandleNewDeviceVerificationTest( + CustomValidatorRequestContext context, + ValidatedTokenRequest request) + { + context.KnownDevice = false; + request.GrantType = "password"; + context.TwoFactorRequired = false; + context.SsoRequired = false; } } diff --git a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs index f7cfd1d394..cbe091a44c 100644 --- a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs +++ b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs @@ -123,6 +123,11 @@ IBaseRequestValidatorTestWrapper Dictionary customResponse) { } + protected override void SetValidationErrorResult( + BaseRequestValidationContextFake context, + CustomValidatorRequestContext requestContext) + { } + protected override Task ValidateContextAsync( BaseRequestValidationContextFake context, CustomValidatorRequestContext validatorContext) From 03dde0d008c87e1be3c563a418c547535be87d20 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Thu, 12 Dec 2024 13:54:04 -0500 Subject: [PATCH 66/94] update copy for domain claimed by organization email (#5138) --- .../AdminConsole/DomainClaimedByOrganization.html.hbs | 5 ++--- .../AdminConsole/DomainClaimedByOrganization.text.hbs | 5 ++--- src/Core/Services/Implementations/HandlebarsMailService.cs | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.html.hbs index 05ca170a50..ad2245e585 100644 --- a/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.html.hbs +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.html.hbs @@ -9,9 +9,8 @@ Here's what that means:
    -
  • This account should only be used to store items related to {{OrganizationName}}
  • -
  • Admins managing your Bitwarden organization manage your email address and other account settings
  • -
  • Admins can also revoke or delete your account at any time
  • +
  • Your administrators can delete your account at any time
  • +
  • You cannot leave the organization
diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.text.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.text.hbs index c0078d389d..b3041a21e9 100644 --- a/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.text.hbs +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.text.hbs @@ -1,8 +1,7 @@ As a member of {{OrganizationName}}, your Bitwarden account is claimed and owned by your organization. Here's what that means: -- This account should only be used to store items related to {{OrganizationName}} -- Your admins managing your Bitwarden organization manages your email address and other account settings -- Your admins can also revoke or delete your account at any time +- Your administrators can delete your account at any time +- You cannot leave the organization For more information, please refer to the following help article: Claimed Accounts (https://bitwarden.com/help/claimed-accounts) diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 22341111f3..deae80c056 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -472,7 +472,7 @@ public class HandlebarsMailService : IMailService "AdminConsole.DomainClaimedByOrganization", new ClaimedDomainUserNotificationViewModel { - TitleFirst = $"Hey {emailAddress}, here is a heads up on your claimed account:", + TitleFirst = $"Hey {emailAddress}, your account is owned by {org.DisplayName()}", OrganizationName = CoreHelpers.SanitizeForEmail(org.DisplayName(), false) }); } From a332a69112c32c729732d9582d2afc2eb7030aea Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Thu, 12 Dec 2024 14:27:31 -0500 Subject: [PATCH 67/94] [PM-14376] Add GET tasks endpoint (#5089) * Added CQRS pattern * Added the GetManyByUserIdAsync signature to the repositiory * Added sql sproc Created user defined type to hold status Created migration file * Added ef core query * Added absract and concrete implementation for GetManyByUserIdStatusAsync * Added integration tests * Updated params to status * Implemented new query to utilize repository method * Added controller for the security task endpoint * Fixed lint issues * Added documentation * simplified to require single status modified script to check for users with edit rights * Updated ef core query * Added new assertions * simplified to require single status * fixed formatting * Fixed sql script * Removed default null * Added security tasks feature flag --- .../Controllers/SecurityTaskController.cs | 40 +++++++ .../Response/SecurityTasksResponseModel.cs | 30 +++++ .../Queries/GetTaskDetailsForUserQuery.cs | 13 +++ .../Queries/IGetTaskDetailsForUserQuery.cs | 15 +++ .../Repositories/ISecurityTaskRepository.cs | 9 +- .../Vault/VaultServiceCollectionExtensions.cs | 1 + .../Repositories/SecurityTaskRepository.cs | 19 +++- .../SecurityTaskReadByUserIdStatusQuery.cs | 90 +++++++++++++++ .../Repositories/SecurityTaskRepository.cs | 14 +++ .../SecurityTask_ReadByUserIdStatus.sql | 56 ++++++++++ .../Comparers/SecurityTaskComparer.cs | 22 ++++ .../SecurityTaskRepositoryTests.cs | 103 ++++++++++++++++++ ...1-21_00_SecurityTaskReadByUserIdStatus.sql | 59 ++++++++++ 13 files changed, 469 insertions(+), 2 deletions(-) create mode 100644 src/Api/Vault/Controllers/SecurityTaskController.cs create mode 100644 src/Api/Vault/Models/Response/SecurityTasksResponseModel.cs create mode 100644 src/Core/Vault/Queries/GetTaskDetailsForUserQuery.cs create mode 100644 src/Core/Vault/Queries/IGetTaskDetailsForUserQuery.cs create mode 100644 src/Infrastructure.EntityFramework/Vault/Repositories/Queries/SecurityTaskReadByUserIdStatusQuery.cs create mode 100644 src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadByUserIdStatus.sql create mode 100644 test/Infrastructure.IntegrationTest/Comparers/SecurityTaskComparer.cs create mode 100644 util/Migrator/DbScripts/2024-11-21_00_SecurityTaskReadByUserIdStatus.sql diff --git a/src/Api/Vault/Controllers/SecurityTaskController.cs b/src/Api/Vault/Controllers/SecurityTaskController.cs new file mode 100644 index 0000000000..7b0bfa0bfb --- /dev/null +++ b/src/Api/Vault/Controllers/SecurityTaskController.cs @@ -0,0 +1,40 @@ +using Bit.Api.Models.Response; +using Bit.Api.Vault.Models.Response; +using Bit.Core; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Bit.Core.Vault.Enums; +using Bit.Core.Vault.Queries; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Vault.Controllers; + +[Route("tasks")] +[Authorize("Application")] +[RequireFeature(FeatureFlagKeys.SecurityTasks)] +public class SecurityTaskController : Controller +{ + private readonly IUserService _userService; + private readonly IGetTaskDetailsForUserQuery _getTaskDetailsForUserQuery; + + public SecurityTaskController(IUserService userService, IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery) + { + _userService = userService; + _getTaskDetailsForUserQuery = getTaskDetailsForUserQuery; + } + + /// + /// Retrieves security tasks for the current user. + /// + /// Optional filter for task status. If not provided returns tasks of all statuses. + /// A list response model containing the security tasks for the user. + [HttpGet("")] + public async Task> Get([FromQuery] SecurityTaskStatus? status) + { + var userId = _userService.GetProperUserId(User).Value; + var securityTasks = await _getTaskDetailsForUserQuery.GetTaskDetailsForUserAsync(userId, status); + var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList(); + return new ListResponseModel(response); + } +} diff --git a/src/Api/Vault/Models/Response/SecurityTasksResponseModel.cs b/src/Api/Vault/Models/Response/SecurityTasksResponseModel.cs new file mode 100644 index 0000000000..c41c54b983 --- /dev/null +++ b/src/Api/Vault/Models/Response/SecurityTasksResponseModel.cs @@ -0,0 +1,30 @@ +using Bit.Core.Models.Api; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; + +namespace Bit.Api.Vault.Models.Response; + +public class SecurityTasksResponseModel : ResponseModel +{ + public SecurityTasksResponseModel(SecurityTask securityTask, string obj = "securityTask") + : base(obj) + { + ArgumentNullException.ThrowIfNull(securityTask); + + Id = securityTask.Id; + OrganizationId = securityTask.OrganizationId; + CipherId = securityTask.CipherId; + Type = securityTask.Type; + Status = securityTask.Status; + CreationDate = securityTask.CreationDate; + RevisionDate = securityTask.RevisionDate; + } + + public Guid Id { get; set; } + public Guid OrganizationId { get; set; } + public Guid? CipherId { get; set; } + public SecurityTaskType Type { get; set; } + public SecurityTaskStatus Status { get; set; } + public DateTime CreationDate { get; set; } + public DateTime RevisionDate { get; set; } +} diff --git a/src/Core/Vault/Queries/GetTaskDetailsForUserQuery.cs b/src/Core/Vault/Queries/GetTaskDetailsForUserQuery.cs new file mode 100644 index 0000000000..976f8fb0ca --- /dev/null +++ b/src/Core/Vault/Queries/GetTaskDetailsForUserQuery.cs @@ -0,0 +1,13 @@ +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; +using Bit.Core.Vault.Repositories; + +namespace Bit.Core.Vault.Queries; + +public class GetTaskDetailsForUserQuery(ISecurityTaskRepository securityTaskRepository) : IGetTaskDetailsForUserQuery +{ + /// + public async Task> GetTaskDetailsForUserAsync(Guid userId, + SecurityTaskStatus? status = null) + => await securityTaskRepository.GetManyByUserIdStatusAsync(userId, status); +} diff --git a/src/Core/Vault/Queries/IGetTaskDetailsForUserQuery.cs b/src/Core/Vault/Queries/IGetTaskDetailsForUserQuery.cs new file mode 100644 index 0000000000..14733c3188 --- /dev/null +++ b/src/Core/Vault/Queries/IGetTaskDetailsForUserQuery.cs @@ -0,0 +1,15 @@ +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; + +namespace Bit.Core.Vault.Queries; + +public interface IGetTaskDetailsForUserQuery +{ + /// + /// Retrieves security tasks for a user based on their organization and cipher access permissions. + /// + /// The Id of the user retrieving tasks + /// Optional filter for task status. If not provided, returns tasks of all statuses + /// A collection of security tasks + Task> GetTaskDetailsForUserAsync(Guid userId, SecurityTaskStatus? status = null); +} diff --git a/src/Core/Vault/Repositories/ISecurityTaskRepository.cs b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs index f2262f207a..34f1f2ee64 100644 --- a/src/Core/Vault/Repositories/ISecurityTaskRepository.cs +++ b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs @@ -1,9 +1,16 @@ using Bit.Core.Repositories; using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; namespace Bit.Core.Vault.Repositories; public interface ISecurityTaskRepository : IRepository { - + /// + /// Retrieves security tasks for a user based on their organization and cipher access permissions. + /// + /// The Id of the user retrieving tasks + /// Optional filter for task status. If not provided, returns tasks of all statuses + /// + Task> GetManyByUserIdStatusAsync(Guid userId, SecurityTaskStatus? status = null); } diff --git a/src/Core/Vault/VaultServiceCollectionExtensions.cs b/src/Core/Vault/VaultServiceCollectionExtensions.cs index 5296f47e3e..d3c9dd9648 100644 --- a/src/Core/Vault/VaultServiceCollectionExtensions.cs +++ b/src/Core/Vault/VaultServiceCollectionExtensions.cs @@ -15,5 +15,6 @@ public static class VaultServiceCollectionExtensions private static void AddVaultQueries(this IServiceCollection services) { services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs index 1674b965f0..dfe8a04814 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs @@ -1,7 +1,11 @@ -using Bit.Core.Settings; +using System.Data; +using Bit.Core.Settings; using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; using Bit.Core.Vault.Repositories; using Bit.Infrastructure.Dapper.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; namespace Bit.Infrastructure.Dapper.Vault.Repositories; @@ -15,4 +19,17 @@ public class SecurityTaskRepository : Repository, ISecurityT : base(connectionString, readOnlyConnectionString) { } + /// + public async Task> GetManyByUserIdStatusAsync(Guid userId, + SecurityTaskStatus? status = null) + { + await using var connection = new SqlConnection(ConnectionString); + + var results = await connection.QueryAsync( + $"[{Schema}].[SecurityTask_ReadByUserIdStatus]", + new { UserId = userId, Status = status }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } } diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/SecurityTaskReadByUserIdStatusQuery.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/SecurityTaskReadByUserIdStatusQuery.cs new file mode 100644 index 0000000000..73f4249542 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/SecurityTaskReadByUserIdStatusQuery.cs @@ -0,0 +1,90 @@ +using Bit.Core.Enums; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories.Queries; + +namespace Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries; + +public class SecurityTaskReadByUserIdStatusQuery : IQuery +{ + private readonly Guid _userId; + private readonly SecurityTaskStatus? _status; + + public SecurityTaskReadByUserIdStatusQuery(Guid userId, SecurityTaskStatus? status) + { + _userId = userId; + _status = status; + } + + public IQueryable Run(DatabaseContext dbContext) + { + var query = from st in dbContext.SecurityTasks + + join ou in dbContext.OrganizationUsers + on st.OrganizationId equals ou.OrganizationId + + join o in dbContext.Organizations + on st.OrganizationId equals o.Id + + join c in dbContext.Ciphers + on st.CipherId equals c.Id into c_g + from c in c_g.DefaultIfEmpty() + + join cc in dbContext.CollectionCiphers + on c.Id equals cc.CipherId into cc_g + from cc in cc_g.DefaultIfEmpty() + + join cu in dbContext.CollectionUsers + on new { cc.CollectionId, OrganizationUserId = ou.Id } equals + new { cu.CollectionId, cu.OrganizationUserId } into cu_g + from cu in cu_g.DefaultIfEmpty() + + join gu in dbContext.GroupUsers + on new { CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals + new { CollectionId = (Guid?)null, gu.OrganizationUserId } into gu_g + from gu in gu_g.DefaultIfEmpty() + + join cg in dbContext.CollectionGroups + on new { cc.CollectionId, gu.GroupId } equals + new { cg.CollectionId, cg.GroupId } into cg_g + from cg in cg_g.DefaultIfEmpty() + + where + ou.UserId == _userId && + ou.Status == OrganizationUserStatusType.Confirmed && + o.Enabled && + ( + st.CipherId == null || + ( + c != null && + ( + (cu != null && !cu.ReadOnly) || (cg != null && !cg.ReadOnly && cu == null) + ) + ) + ) && + (_status == null || st.Status == _status) + group st by new + { + st.Id, + st.OrganizationId, + st.CipherId, + st.Type, + st.Status, + st.CreationDate, + st.RevisionDate + } into g + select new SecurityTask + { + Id = g.Key.Id, + OrganizationId = g.Key.OrganizationId, + CipherId = g.Key.CipherId, + Type = g.Key.Type, + Status = g.Key.Status, + CreationDate = g.Key.CreationDate, + RevisionDate = g.Key.RevisionDate + }; + + return query.OrderByDescending(st => st.CreationDate); + } +} diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs index 82c06bcc6b..bd56df1bcf 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs @@ -1,7 +1,10 @@ using AutoMapper; +using Bit.Core.Vault.Enums; using Bit.Core.Vault.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Vault.Models; +using Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace Bit.Infrastructure.EntityFramework.Vault.Repositories; @@ -11,4 +14,15 @@ public class SecurityTaskRepository : Repository context.SecurityTasks) { } + + /// + public async Task> GetManyByUserIdStatusAsync(Guid userId, + SecurityTaskStatus? status = null) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var query = new SecurityTaskReadByUserIdStatusQuery(userId, status); + var data = await query.Run(dbContext).ToListAsync(); + return data; + } } diff --git a/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadByUserIdStatus.sql b/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadByUserIdStatus.sql new file mode 100644 index 0000000000..2a4ecdb4c1 --- /dev/null +++ b/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadByUserIdStatus.sql @@ -0,0 +1,56 @@ +CREATE PROCEDURE [dbo].[SecurityTask_ReadByUserIdStatus] + @UserId UNIQUEIDENTIFIER, + @Status TINYINT = NULL +AS +BEGIN + SET NOCOUNT ON + + SELECT + ST.Id, + ST.OrganizationId, + ST.CipherId, + ST.Type, + ST.Status, + ST.CreationDate, + ST.RevisionDate + FROM + [dbo].[SecurityTaskView] ST + INNER JOIN + [dbo].[OrganizationUserView] OU ON OU.[OrganizationId] = ST.[OrganizationId] + INNER JOIN + [dbo].[Organization] O ON O.[Id] = ST.[OrganizationId] + LEFT JOIN + [dbo].[CipherView] C ON C.[Id] = ST.[CipherId] + LEFT JOIN + [dbo].[CollectionCipher] CC ON CC.[CipherId] = C.[Id] AND C.[Id] IS NOT NULL + LEFT JOIN + [dbo].[CollectionUser] CU ON CU.[CollectionId] = CC.[CollectionId] AND CU.[OrganizationUserId] = OU.[Id] AND C.[Id] IS NOT NULL + LEFT JOIN + [dbo].[GroupUser] GU ON GU.[OrganizationUserId] = OU.[Id] AND CU.[CollectionId] IS NULL AND C.[Id] IS NOT NULL + LEFT JOIN + [dbo].[CollectionGroup] CG ON CG.[GroupId] = GU.[GroupId] AND CG.[CollectionId] = CC.[CollectionId] + WHERE + OU.[UserId] = @UserId + AND OU.[Status] = 2 -- Ensure user is confirmed + AND O.[Enabled] = 1 + AND ( + ST.[CipherId] IS NULL + OR ( + C.[Id] IS NOT NULL + AND ( + CU.[ReadOnly] = 0 + OR CG.[ReadOnly] = 0 + ) + ) + ) + AND ST.[Status] = COALESCE(@Status, ST.[Status]) + GROUP BY + ST.Id, + ST.OrganizationId, + ST.CipherId, + ST.Type, + ST.Status, + ST.CreationDate, + ST.RevisionDate + ORDER BY ST.[CreationDate] DESC +END diff --git a/test/Infrastructure.IntegrationTest/Comparers/SecurityTaskComparer.cs b/test/Infrastructure.IntegrationTest/Comparers/SecurityTaskComparer.cs new file mode 100644 index 0000000000..847896d3a0 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/Comparers/SecurityTaskComparer.cs @@ -0,0 +1,22 @@ +using System.Diagnostics.CodeAnalysis; +using Bit.Core.Vault.Entities; + +namespace Bit.Infrastructure.IntegrationTest.Comparers; + +/// +/// Determines the equality of two SecurityTask objects. +/// +public class SecurityTaskComparer : IEqualityComparer +{ + public bool Equals(SecurityTask x, SecurityTask y) + { + return x.Id.Equals(y.Id) && + x.Type.Equals(y.Type) && + x.Status.Equals(y.Status); + } + + public int GetHashCode([DisallowNull] SecurityTask obj) + { + return base.GetHashCode(); + } +} diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs index 79cc1d2bc9..2010c90a5e 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs @@ -1,9 +1,13 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Enums; using Bit.Core.Vault.Repositories; +using Bit.Infrastructure.IntegrationTest.Comparers; using Xunit; namespace Bit.Infrastructure.IntegrationTest.Vault.Repositories; @@ -120,4 +124,103 @@ public class SecurityTaskRepositoryTests Assert.Equal(task.Id, updatedTask.Id); Assert.Equal(SecurityTaskStatus.Completed, updatedTask.Status); } + + [DatabaseTheory, DatabaseData] + public async Task GetManyByUserIdAsync_ReturnsExpectedTasks( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + ICipherRepository cipherRepository, + ISecurityTaskRepository securityTaskRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + PlanType = PlanType.EnterpriseAnnually, + Plan = "Test Plan", + BillingEmail = "billing@email.com" + }); + + var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed + }); + + var collection = await collectionRepository.CreateAsync(new Collection + { + OrganizationId = organization.Id, + Name = "Test Collection 1", + }); + + var collection2 = await collectionRepository.CreateAsync(new Collection + { + OrganizationId = organization.Id, + Name = "Test Collection 2", + }); + + var cipher1 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", }; + await cipherRepository.CreateAsync(cipher1, [collection.Id, collection2.Id]); + + var cipher2 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", }; + await cipherRepository.CreateAsync(cipher2, [collection.Id]); + + var task1 = await securityTaskRepository.CreateAsync(new SecurityTask + { + OrganizationId = organization.Id, + CipherId = cipher1.Id, + Status = SecurityTaskStatus.Pending, + Type = SecurityTaskType.UpdateAtRiskCredential, + }); + + var task2 = await securityTaskRepository.CreateAsync(new SecurityTask + { + OrganizationId = organization.Id, + CipherId = cipher2.Id, + Status = SecurityTaskStatus.Completed, + Type = SecurityTaskType.UpdateAtRiskCredential, + }); + + var task3 = await securityTaskRepository.CreateAsync(new SecurityTask + { + OrganizationId = organization.Id, + CipherId = cipher2.Id, + Status = SecurityTaskStatus.Pending, + Type = SecurityTaskType.UpdateAtRiskCredential, + }); + + await collectionRepository.UpdateUsersAsync(collection.Id, + new List + { + new() {Id = orgUser.Id, ReadOnly = false, HidePasswords = false, Manage = true} + }); + + var allTasks = await securityTaskRepository.GetManyByUserIdStatusAsync(user.Id); + Assert.Equal(3, allTasks.Count); + Assert.Contains(task1, allTasks, new SecurityTaskComparer()); + Assert.Contains(task2, allTasks, new SecurityTaskComparer()); + Assert.Contains(task3, allTasks, new SecurityTaskComparer()); + + var pendingTasks = await securityTaskRepository.GetManyByUserIdStatusAsync(user.Id, SecurityTaskStatus.Pending); + Assert.Equal(2, pendingTasks.Count); + Assert.Contains(task1, pendingTasks, new SecurityTaskComparer()); + Assert.Contains(task3, pendingTasks, new SecurityTaskComparer()); + Assert.DoesNotContain(task2, pendingTasks, new SecurityTaskComparer()); + + var completedTasks = await securityTaskRepository.GetManyByUserIdStatusAsync(user.Id, SecurityTaskStatus.Completed); + Assert.Single(completedTasks); + Assert.Contains(task2, completedTasks, new SecurityTaskComparer()); + Assert.DoesNotContain(task1, completedTasks, new SecurityTaskComparer()); + Assert.DoesNotContain(task3, completedTasks, new SecurityTaskComparer()); + } } diff --git a/util/Migrator/DbScripts/2024-11-21_00_SecurityTaskReadByUserIdStatus.sql b/util/Migrator/DbScripts/2024-11-21_00_SecurityTaskReadByUserIdStatus.sql new file mode 100644 index 0000000000..a5760227cb --- /dev/null +++ b/util/Migrator/DbScripts/2024-11-21_00_SecurityTaskReadByUserIdStatus.sql @@ -0,0 +1,59 @@ +-- Security Task Read By UserId Status +-- Stored Procedure: ReadByUserIdStatus +CREATE OR ALTER PROCEDURE [dbo].[SecurityTask_ReadByUserIdStatus] + @UserId UNIQUEIDENTIFIER, + @Status TINYINT = NULL +AS +BEGIN + SET NOCOUNT ON + + SELECT + ST.Id, + ST.OrganizationId, + ST.CipherId, + ST.Type, + ST.Status, + ST.CreationDate, + ST.RevisionDate + FROM + [dbo].[SecurityTaskView] ST + INNER JOIN + [dbo].[OrganizationUserView] OU ON OU.[OrganizationId] = ST.[OrganizationId] + INNER JOIN + [dbo].[Organization] O ON O.[Id] = ST.[OrganizationId] + LEFT JOIN + [dbo].[CipherView] C ON C.[Id] = ST.[CipherId] + LEFT JOIN + [dbo].[CollectionCipher] CC ON CC.[CipherId] = C.[Id] AND C.[Id] IS NOT NULL + LEFT JOIN + [dbo].[CollectionUser] CU ON CU.[CollectionId] = CC.[CollectionId] AND CU.[OrganizationUserId] = OU.[Id] AND C.[Id] IS NOT NULL + LEFT JOIN + [dbo].[GroupUser] GU ON GU.[OrganizationUserId] = OU.[Id] AND CU.[CollectionId] IS NULL AND C.[Id] IS NOT NULL + LEFT JOIN + [dbo].[CollectionGroup] CG ON CG.[GroupId] = GU.[GroupId] AND CG.[CollectionId] = CC.[CollectionId] + WHERE + OU.[UserId] = @UserId + AND OU.[Status] = 2 -- Ensure user is confirmed + AND O.[Enabled] = 1 + AND ( + ST.[CipherId] IS NULL + OR ( + C.[Id] IS NOT NULL + AND ( + CU.[ReadOnly] = 0 + OR CG.[ReadOnly] = 0 + ) + ) + ) + AND ST.[Status] = COALESCE(@Status, ST.[Status]) + GROUP BY + ST.Id, + ST.OrganizationId, + ST.CipherId, + ST.Type, + ST.Status, + ST.CreationDate, + ST.RevisionDate + ORDER BY ST.[CreationDate] DESC +END +GO From a3174cffd4555b8ed743ba014ccd7b3822a4199a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:52:28 -0800 Subject: [PATCH 68/94] [deps] Auth: Update webpack to v5.97.1 (#5018) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bitwarden_license/src/Sso/package-lock.json | 214 +++++++++++--------- bitwarden_license/src/Sso/package.json | 2 +- src/Admin/package-lock.json | 214 +++++++++++--------- src/Admin/package.json | 2 +- 4 files changed, 228 insertions(+), 204 deletions(-) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index f9f0a90e94..cb95485b88 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -19,7 +19,7 @@ "mini-css-extract-plugin": "2.9.1", "sass": "1.79.5", "sass-loader": "16.0.2", - "webpack": "5.95.0", + "webpack": "5.97.1", "webpack-cli": "5.1.4" } }, @@ -394,6 +394,28 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -419,73 +441,73 @@ } }, "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "license": "MIT", "dependencies": { @@ -493,9 +515,9 @@ } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -503,79 +525,79 @@ } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -641,9 +663,9 @@ "license": "Apache-2.0" }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", "bin": { @@ -653,16 +675,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -2216,19 +2228,19 @@ } }, "node_modules/webpack": { - "version": "5.95.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", - "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", diff --git a/bitwarden_license/src/Sso/package.json b/bitwarden_license/src/Sso/package.json index 29dee20ad8..9f488d6be8 100644 --- a/bitwarden_license/src/Sso/package.json +++ b/bitwarden_license/src/Sso/package.json @@ -18,7 +18,7 @@ "mini-css-extract-plugin": "2.9.1", "sass": "1.79.5", "sass-loader": "16.0.2", - "webpack": "5.95.0", + "webpack": "5.97.1", "webpack-cli": "5.1.4" } } diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index a9256a9a4d..f9976a8467 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -20,7 +20,7 @@ "mini-css-extract-plugin": "2.9.1", "sass": "1.79.5", "sass-loader": "16.0.2", - "webpack": "5.95.0", + "webpack": "5.97.1", "webpack-cli": "5.1.4" } }, @@ -395,6 +395,28 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -420,73 +442,73 @@ } }, "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "license": "MIT", "dependencies": { @@ -494,9 +516,9 @@ } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -504,79 +526,79 @@ } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -642,9 +664,9 @@ "license": "Apache-2.0" }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", "bin": { @@ -654,16 +676,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -2225,19 +2237,19 @@ } }, "node_modules/webpack": { - "version": "5.95.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", - "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", diff --git a/src/Admin/package.json b/src/Admin/package.json index 6984e99814..d0d9018dad 100644 --- a/src/Admin/package.json +++ b/src/Admin/package.json @@ -19,7 +19,7 @@ "mini-css-extract-plugin": "2.9.1", "sass": "1.79.5", "sass-loader": "16.0.2", - "webpack": "5.95.0", + "webpack": "5.97.1", "webpack-cli": "5.1.4" } } From e0d82c447da4a994c9b55b7d7e97fa4daab14035 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:57:00 -0800 Subject: [PATCH 69/94] [deps] Auth: Update sass-loader to v16.0.4 (#5014) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bitwarden_license/src/Sso/package-lock.json | 8 ++++---- bitwarden_license/src/Sso/package.json | 2 +- src/Admin/package-lock.json | 8 ++++---- src/Admin/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index cb95485b88..c403e35d4b 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -18,7 +18,7 @@ "expose-loader": "5.0.0", "mini-css-extract-plugin": "2.9.1", "sass": "1.79.5", - "sass-loader": "16.0.2", + "sass-loader": "16.0.4", "webpack": "5.97.1", "webpack-cli": "5.1.4" } @@ -1849,9 +1849,9 @@ } }, "node_modules/sass-loader": { - "version": "16.0.2", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.2.tgz", - "integrity": "sha512-Ll6iXZ1EYwYT19SqW4mSBb76vSSi8JgzElmzIerhEGgzB5hRjDQIWsPmuk1UrAXkR16KJHqVY0eH+5/uw9Tmfw==", + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", + "integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/bitwarden_license/src/Sso/package.json b/bitwarden_license/src/Sso/package.json index 9f488d6be8..312adea945 100644 --- a/bitwarden_license/src/Sso/package.json +++ b/bitwarden_license/src/Sso/package.json @@ -17,7 +17,7 @@ "expose-loader": "5.0.0", "mini-css-extract-plugin": "2.9.1", "sass": "1.79.5", - "sass-loader": "16.0.2", + "sass-loader": "16.0.4", "webpack": "5.97.1", "webpack-cli": "5.1.4" } diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index f9976a8467..8cc83c6e2d 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -19,7 +19,7 @@ "expose-loader": "5.0.0", "mini-css-extract-plugin": "2.9.1", "sass": "1.79.5", - "sass-loader": "16.0.2", + "sass-loader": "16.0.4", "webpack": "5.97.1", "webpack-cli": "5.1.4" } @@ -1850,9 +1850,9 @@ } }, "node_modules/sass-loader": { - "version": "16.0.2", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.2.tgz", - "integrity": "sha512-Ll6iXZ1EYwYT19SqW4mSBb76vSSi8JgzElmzIerhEGgzB5hRjDQIWsPmuk1UrAXkR16KJHqVY0eH+5/uw9Tmfw==", + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", + "integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/Admin/package.json b/src/Admin/package.json index d0d9018dad..ba0bc954d8 100644 --- a/src/Admin/package.json +++ b/src/Admin/package.json @@ -18,7 +18,7 @@ "expose-loader": "5.0.0", "mini-css-extract-plugin": "2.9.1", "sass": "1.79.5", - "sass-loader": "16.0.2", + "sass-loader": "16.0.4", "webpack": "5.97.1", "webpack-cli": "5.1.4" } From ce60657b8e72c94a898a7f6fc93b0f5f6d9fe0d4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:04:33 -0800 Subject: [PATCH 70/94] [deps] Auth: Update mini-css-extract-plugin to v2.9.2 (#5013) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bitwarden_license/src/Sso/package-lock.json | 8 ++++---- bitwarden_license/src/Sso/package.json | 2 +- src/Admin/package-lock.json | 8 ++++---- src/Admin/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index c403e35d4b..c23e3b67da 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -16,7 +16,7 @@ "devDependencies": { "css-loader": "7.1.2", "expose-loader": "5.0.0", - "mini-css-extract-plugin": "2.9.1", + "mini-css-extract-plugin": "2.9.2", "sass": "1.79.5", "sass-loader": "16.0.4", "webpack": "5.97.1", @@ -1438,9 +1438,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.1.tgz", - "integrity": "sha512-+Vyi+GCCOHnrJ2VPS+6aPoXN2k2jgUzDRhTFLjjTBn23qyXJXkjUWQgTL+mXpF5/A8ixLdCc6kWsoeOjKGejKQ==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", + "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/bitwarden_license/src/Sso/package.json b/bitwarden_license/src/Sso/package.json index 312adea945..fa1fac3907 100644 --- a/bitwarden_license/src/Sso/package.json +++ b/bitwarden_license/src/Sso/package.json @@ -15,7 +15,7 @@ "devDependencies": { "css-loader": "7.1.2", "expose-loader": "5.0.0", - "mini-css-extract-plugin": "2.9.1", + "mini-css-extract-plugin": "2.9.2", "sass": "1.79.5", "sass-loader": "16.0.4", "webpack": "5.97.1", diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index 8cc83c6e2d..203e574717 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -17,7 +17,7 @@ "devDependencies": { "css-loader": "7.1.2", "expose-loader": "5.0.0", - "mini-css-extract-plugin": "2.9.1", + "mini-css-extract-plugin": "2.9.2", "sass": "1.79.5", "sass-loader": "16.0.4", "webpack": "5.97.1", @@ -1439,9 +1439,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.1.tgz", - "integrity": "sha512-+Vyi+GCCOHnrJ2VPS+6aPoXN2k2jgUzDRhTFLjjTBn23qyXJXkjUWQgTL+mXpF5/A8ixLdCc6kWsoeOjKGejKQ==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", + "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/Admin/package.json b/src/Admin/package.json index ba0bc954d8..2a8e91f43e 100644 --- a/src/Admin/package.json +++ b/src/Admin/package.json @@ -16,7 +16,7 @@ "devDependencies": { "css-loader": "7.1.2", "expose-loader": "5.0.0", - "mini-css-extract-plugin": "2.9.1", + "mini-css-extract-plugin": "2.9.2", "sass": "1.79.5", "sass-loader": "16.0.4", "webpack": "5.97.1", From 6d9c8d0a474dedcd525d57549832c494fc95a8ac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:04:18 -0800 Subject: [PATCH 71/94] [deps] Auth: Lock file maintenance (#4952) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bitwarden_license/src/Sso/package-lock.json | 246 +++++++++++--------- src/Admin/package-lock.json | 246 +++++++++++--------- 2 files changed, 270 insertions(+), 222 deletions(-) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index c23e3b67da..67fc4d71f1 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -34,9 +34,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, "license": "MIT", "dependencies": { @@ -98,10 +98,11 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", - "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", + "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", "dev": true, + "hasInstallScript": true, "license": "MIT", "dependencies": { "detect-libc": "^1.0.3", @@ -117,24 +118,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.4.1", - "@parcel/watcher-darwin-arm64": "2.4.1", - "@parcel/watcher-darwin-x64": "2.4.1", - "@parcel/watcher-freebsd-x64": "2.4.1", - "@parcel/watcher-linux-arm-glibc": "2.4.1", - "@parcel/watcher-linux-arm64-glibc": "2.4.1", - "@parcel/watcher-linux-arm64-musl": "2.4.1", - "@parcel/watcher-linux-x64-glibc": "2.4.1", - "@parcel/watcher-linux-x64-musl": "2.4.1", - "@parcel/watcher-win32-arm64": "2.4.1", - "@parcel/watcher-win32-ia32": "2.4.1", - "@parcel/watcher-win32-x64": "2.4.1" + "@parcel/watcher-android-arm64": "2.5.0", + "@parcel/watcher-darwin-arm64": "2.5.0", + "@parcel/watcher-darwin-x64": "2.5.0", + "@parcel/watcher-freebsd-x64": "2.5.0", + "@parcel/watcher-linux-arm-glibc": "2.5.0", + "@parcel/watcher-linux-arm-musl": "2.5.0", + "@parcel/watcher-linux-arm64-glibc": "2.5.0", + "@parcel/watcher-linux-arm64-musl": "2.5.0", + "@parcel/watcher-linux-x64-glibc": "2.5.0", + "@parcel/watcher-linux-x64-musl": "2.5.0", + "@parcel/watcher-win32-arm64": "2.5.0", + "@parcel/watcher-win32-ia32": "2.5.0", + "@parcel/watcher-win32-x64": "2.5.0" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", - "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", + "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", "cpu": [ "arm64" ], @@ -153,9 +155,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", - "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", "cpu": [ "arm64" ], @@ -174,9 +176,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", - "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", + "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", "cpu": [ "x64" ], @@ -195,9 +197,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", - "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", + "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", "cpu": [ "x64" ], @@ -216,9 +218,30 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", - "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", + "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", + "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", "cpu": [ "arm" ], @@ -237,9 +260,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", - "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", + "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", "cpu": [ "arm64" ], @@ -258,9 +281,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", - "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", "cpu": [ "arm64" ], @@ -279,9 +302,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", - "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", + "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", "cpu": [ "x64" ], @@ -300,9 +323,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", - "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", "cpu": [ "x64" ], @@ -321,9 +344,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", - "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", + "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", "cpu": [ "arm64" ], @@ -342,9 +365,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", - "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", + "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", "cpu": [ "ia32" ], @@ -363,9 +386,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", - "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", + "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", "cpu": [ "x64" ], @@ -431,13 +454,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", - "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, "node_modules/@webassemblyjs/ast": { @@ -737,6 +760,7 @@ "url": "https://opencollective.com/bootstrap" } ], + "license": "MIT", "peerDependencies": { "@popperjs/core": "^2.11.8" } @@ -755,9 +779,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", - "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "dev": true, "funding": [ { @@ -775,10 +799,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001663", - "electron-to-chromium": "^1.5.28", + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -795,9 +819,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001668", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", - "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==", + "version": "1.0.30001688", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz", + "integrity": "sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==", "dev": true, "funding": [ { @@ -871,9 +895,9 @@ "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -948,9 +972,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.36", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz", - "integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==", + "version": "1.5.73", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz", + "integrity": "sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==", "dev": true, "license": "ISC" }, @@ -1087,11 +1111,11 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", - "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", "dev": true, - "license": "MIT" + "license": "BSD-3-Clause" }, "node_modules/fastest-levenshtein": { "version": "1.0.16", @@ -1459,9 +1483,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -1492,9 +1516,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true, "license": "MIT" }, @@ -1565,9 +1589,9 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, @@ -1598,9 +1622,9 @@ } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "dev": true, "funding": [ { @@ -1619,7 +1643,7 @@ "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -1640,14 +1664,14 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", - "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", "dev": true, "license": "MIT", "dependencies": { "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", + "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "engines": { @@ -1658,13 +1682,13 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", - "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", "dev": true, "license": "ISC", "dependencies": { - "postcss-selector-parser": "^6.0.4" + "postcss-selector-parser": "^7.0.0" }, "engines": { "node": "^10 || ^12 || >= 14" @@ -1690,9 +1714,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1890,9 +1914,9 @@ } }, "node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "dev": true, "license": "MIT", "dependencies": { @@ -1902,7 +1926,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -2039,9 +2063,9 @@ } }, "node_modules/terser": { - "version": "5.34.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz", - "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==", + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2159,9 +2183,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" }, diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index 203e574717..e792106499 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -35,9 +35,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, "license": "MIT", "dependencies": { @@ -99,10 +99,11 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", - "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", + "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", "dev": true, + "hasInstallScript": true, "license": "MIT", "dependencies": { "detect-libc": "^1.0.3", @@ -118,24 +119,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.4.1", - "@parcel/watcher-darwin-arm64": "2.4.1", - "@parcel/watcher-darwin-x64": "2.4.1", - "@parcel/watcher-freebsd-x64": "2.4.1", - "@parcel/watcher-linux-arm-glibc": "2.4.1", - "@parcel/watcher-linux-arm64-glibc": "2.4.1", - "@parcel/watcher-linux-arm64-musl": "2.4.1", - "@parcel/watcher-linux-x64-glibc": "2.4.1", - "@parcel/watcher-linux-x64-musl": "2.4.1", - "@parcel/watcher-win32-arm64": "2.4.1", - "@parcel/watcher-win32-ia32": "2.4.1", - "@parcel/watcher-win32-x64": "2.4.1" + "@parcel/watcher-android-arm64": "2.5.0", + "@parcel/watcher-darwin-arm64": "2.5.0", + "@parcel/watcher-darwin-x64": "2.5.0", + "@parcel/watcher-freebsd-x64": "2.5.0", + "@parcel/watcher-linux-arm-glibc": "2.5.0", + "@parcel/watcher-linux-arm-musl": "2.5.0", + "@parcel/watcher-linux-arm64-glibc": "2.5.0", + "@parcel/watcher-linux-arm64-musl": "2.5.0", + "@parcel/watcher-linux-x64-glibc": "2.5.0", + "@parcel/watcher-linux-x64-musl": "2.5.0", + "@parcel/watcher-win32-arm64": "2.5.0", + "@parcel/watcher-win32-ia32": "2.5.0", + "@parcel/watcher-win32-x64": "2.5.0" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", - "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", + "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", "cpu": [ "arm64" ], @@ -154,9 +156,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", - "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", "cpu": [ "arm64" ], @@ -175,9 +177,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", - "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", + "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", "cpu": [ "x64" ], @@ -196,9 +198,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", - "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", + "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", "cpu": [ "x64" ], @@ -217,9 +219,30 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", - "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", + "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", + "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", "cpu": [ "arm" ], @@ -238,9 +261,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", - "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", + "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", "cpu": [ "arm64" ], @@ -259,9 +282,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", - "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", "cpu": [ "arm64" ], @@ -280,9 +303,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", - "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", + "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", "cpu": [ "x64" ], @@ -301,9 +324,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", - "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", "cpu": [ "x64" ], @@ -322,9 +345,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", - "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", + "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", "cpu": [ "arm64" ], @@ -343,9 +366,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", - "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", + "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", "cpu": [ "ia32" ], @@ -364,9 +387,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", - "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", + "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", "cpu": [ "x64" ], @@ -432,13 +455,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", - "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, "node_modules/@webassemblyjs/ast": { @@ -738,6 +761,7 @@ "url": "https://opencollective.com/bootstrap" } ], + "license": "MIT", "peerDependencies": { "@popperjs/core": "^2.11.8" } @@ -756,9 +780,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", - "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "dev": true, "funding": [ { @@ -776,10 +800,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001663", - "electron-to-chromium": "^1.5.28", + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -796,9 +820,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001668", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", - "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==", + "version": "1.0.30001688", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz", + "integrity": "sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==", "dev": true, "funding": [ { @@ -872,9 +896,9 @@ "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -949,9 +973,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.36", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz", - "integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==", + "version": "1.5.73", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz", + "integrity": "sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==", "dev": true, "license": "ISC" }, @@ -1088,11 +1112,11 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", - "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", "dev": true, - "license": "MIT" + "license": "BSD-3-Clause" }, "node_modules/fastest-levenshtein": { "version": "1.0.16", @@ -1460,9 +1484,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -1493,9 +1517,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true, "license": "MIT" }, @@ -1566,9 +1590,9 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, @@ -1599,9 +1623,9 @@ } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "dev": true, "funding": [ { @@ -1620,7 +1644,7 @@ "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -1641,14 +1665,14 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", - "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", "dev": true, "license": "MIT", "dependencies": { "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", + "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "engines": { @@ -1659,13 +1683,13 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", - "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", "dev": true, "license": "ISC", "dependencies": { - "postcss-selector-parser": "^6.0.4" + "postcss-selector-parser": "^7.0.0" }, "engines": { "node": "^10 || ^12 || >= 14" @@ -1691,9 +1715,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1891,9 +1915,9 @@ } }, "node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "dev": true, "license": "MIT", "dependencies": { @@ -1903,7 +1927,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -2040,9 +2064,9 @@ } }, "node_modules/terser": { - "version": "5.34.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz", - "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==", + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2168,9 +2192,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" }, From 6da7fdc39e6076ab7b181c4cb19805d5266258b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:32:29 +0000 Subject: [PATCH 72/94] =?UTF-8?q?[PM-15547]=C2=A0Revoke=20managed=20user?= =?UTF-8?q?=20on=202FA=20removal=20if=20enforced=20by=20organization=20pol?= =?UTF-8?q?icy=20(#5124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revoke managed user on 2FA removal if enforced by organization policy * Rename TwoFactorDisabling to TwoFactorDisabled in EventSystemUser enum --- .../AdminConsole/Enums/EventSystemUser.cs | 1 + .../Services/Implementations/UserService.cs | 28 ++- test/Core.Test/Services/UserServiceTests.cs | 171 +++++++++++++++++- 3 files changed, 195 insertions(+), 5 deletions(-) diff --git a/src/Core/AdminConsole/Enums/EventSystemUser.cs b/src/Core/AdminConsole/Enums/EventSystemUser.cs index c3e13705dd..1eb1e5b4ab 100644 --- a/src/Core/AdminConsole/Enums/EventSystemUser.cs +++ b/src/Core/AdminConsole/Enums/EventSystemUser.cs @@ -6,4 +6,5 @@ public enum EventSystemUser : byte SCIM = 1, DomainVerification = 2, PublicApi = 3, + TwoFactorDisabled = 4, } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index fa8cd3cef8..2bc81959b9 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1,7 +1,9 @@ using System.Security.Claims; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; @@ -14,6 +16,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.Repositories; using Bit.Core.Settings; @@ -67,6 +70,7 @@ public class UserService : UserManager, IUserService, IDisposable private readonly IFeatureService _featureService; private readonly IPremiumUserBillingService _premiumUserBillingService; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; + private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand; public UserService( IUserRepository userRepository, @@ -101,7 +105,8 @@ public class UserService : UserManager, IUserService, IDisposable IDataProtectorTokenFactory orgUserInviteTokenDataFactory, IFeatureService featureService, IPremiumUserBillingService premiumUserBillingService, - IRemoveOrganizationUserCommand removeOrganizationUserCommand) + IRemoveOrganizationUserCommand removeOrganizationUserCommand, + IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand) : base( store, optionsAccessor, @@ -142,6 +147,7 @@ public class UserService : UserManager, IUserService, IDisposable _featureService = featureService; _premiumUserBillingService = premiumUserBillingService; _removeOrganizationUserCommand = removeOrganizationUserCommand; + _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand; } public Guid? GetProperUserId(ClaimsPrincipal principal) @@ -1355,13 +1361,27 @@ public class UserService : UserManager, IUserService, IDisposable private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user) { var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication); + var organizationsManagingUser = await GetOrganizationsManagingUserAsync(user.Id); var removeOrgUserTasks = twoFactorPolicies.Select(async p => { - await _removeOrganizationUserCommand.RemoveUserAsync(p.OrganizationId, user.Id); var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId); - await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync( - organization.DisplayName(), user.Email); + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && organizationsManagingUser.Any(o => o.Id == p.OrganizationId)) + { + await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync( + new RevokeOrganizationUsersRequest( + p.OrganizationId, + [new OrganizationUserUserDetails { UserId = user.Id, OrganizationId = p.OrganizationId }], + new SystemUser(EventSystemUser.TwoFactorDisabled))); + await _mailService.SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(organization.DisplayName(), user.Email); + } + else + { + await _removeOrganizationUserCommand.RemoveUserAsync(p.OrganizationId, user.Id); + await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync( + organization.DisplayName(), user.Email); + } + }).ToArray(); await Task.WhenAll(removeOrgUserTasks); diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 71cceb86ad..de2a518717 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -1,7 +1,9 @@ using System.Security.Claims; using System.Text.Json; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; @@ -10,13 +12,16 @@ using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tools.Services; +using Bit.Core.Utilities; using Bit.Core.Vault.Repositories; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -268,7 +273,8 @@ public class UserServiceTests new FakeDataProtectorTokenFactory(), sutProvider.GetDependency(), sutProvider.GetDependency(), - sutProvider.GetDependency() + sutProvider.GetDependency(), + sutProvider.GetDependency() ); var actualIsVerified = await sut.VerifySecretAsync(user, secret); @@ -353,6 +359,169 @@ public class UserServiceTests Assert.False(result); } + [Theory, BitAutoData] + public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RemovesUserFromOrganizationAndSendsEmail( + SutProvider sutProvider, User user, Organization organization) + { + // Arrange + user.SetTwoFactorProviders(new Dictionary + { + [TwoFactorProviderType.Email] = new() { Enabled = true } + }); + sutProvider.GetDependency() + .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication) + .Returns( + [ + new OrganizationUserPolicyDetails + { + OrganizationId = organization.Id, + PolicyType = PolicyType.TwoFactorAuthentication, + PolicyEnabled = true + } + ]); + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary(), JsonHelpers.LegacyEnumKeyResolver); + + // Act + await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders)); + await sutProvider.GetDependency() + .Received(1) + .LogUserEventAsync(user.Id, EventType.User_Disabled2fa); + await sutProvider.GetDependency() + .Received(1) + .RemoveUserAsync(organization.Id, user.Id); + await sutProvider.GetDependency() + .Received(1) + .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), user.Email); + } + + [Theory, BitAutoData] + public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_UserHasOneProviderEnabled_DoesNotRemoveUserFromOrganization( + SutProvider sutProvider, User user, Organization organization) + { + // Arrange + user.SetTwoFactorProviders(new Dictionary + { + [TwoFactorProviderType.Email] = new() { Enabled = true }, + [TwoFactorProviderType.Remember] = new() { Enabled = true } + }); + sutProvider.GetDependency() + .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication) + .Returns( + [ + new OrganizationUserPolicyDetails + { + OrganizationId = organization.Id, + PolicyType = PolicyType.TwoFactorAuthentication, + PolicyEnabled = true + } + ]); + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary + { + [TwoFactorProviderType.Remember] = new() { Enabled = true } + }, JsonHelpers.LegacyEnumKeyResolver); + + // Act + await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders)); + await sutProvider.GetDependency() + .Received(1) + .LogUserEventAsync(user.Id, EventType.User_Disabled2fa); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RemoveUserAsync(default, default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(default, default); + } + + [Theory, BitAutoData] + public async Task DisableTwoFactorProviderAsync_WithAccountDeprovisioningEnabled_WhenOrganizationHas2FAPolicyEnabled_WhenUserIsManaged_DisablingAllProviders_RemovesOrRevokesUserAndSendsEmail( + SutProvider sutProvider, User user, Organization organization1, Organization organization2) + { + // Arrange + user.SetTwoFactorProviders(new Dictionary + { + [TwoFactorProviderType.Email] = new() { Enabled = true } + }); + organization1.Enabled = organization2.Enabled = true; + organization1.UseSso = organization2.UseSso = true; + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + sutProvider.GetDependency() + .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication) + .Returns( + [ + new OrganizationUserPolicyDetails + { + OrganizationId = organization1.Id, + PolicyType = PolicyType.TwoFactorAuthentication, + PolicyEnabled = true + }, + new OrganizationUserPolicyDetails + { + OrganizationId = organization2.Id, + PolicyType = PolicyType.TwoFactorAuthentication, + PolicyEnabled = true + } + ]); + sutProvider.GetDependency() + .GetByIdAsync(organization1.Id) + .Returns(organization1); + sutProvider.GetDependency() + .GetByIdAsync(organization2.Id) + .Returns(organization2); + sutProvider.GetDependency() + .GetByVerifiedUserEmailDomainAsync(user.Id) + .Returns(new[] { organization1 }); + var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary(), JsonHelpers.LegacyEnumKeyResolver); + + // Act + await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders)); + await sutProvider.GetDependency() + .Received(1) + .LogUserEventAsync(user.Id, EventType.User_Disabled2fa); + + // Revoke the user from the first organization because they are managed by it + await sutProvider.GetDependency() + .Received(1) + .RevokeNonCompliantOrganizationUsersAsync( + Arg.Is(r => r.OrganizationId == organization1.Id && + r.OrganizationUsers.First().UserId == user.Id && + r.OrganizationUsers.First().OrganizationId == organization1.Id)); + await sutProvider.GetDependency() + .Received(1) + .SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(organization1.DisplayName(), user.Email); + + // Remove the user from the second organization because they are not managed by it + await sutProvider.GetDependency() + .Received(1) + .RemoveUserAsync(organization2.Id, user.Id); + await sutProvider.GetDependency() + .Received(1) + .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization2.DisplayName(), user.Email); + } + private static void SetupUserAndDevice(User user, bool shouldHavePassword) { From a28e517eebe28da5f7d89183bbc8af3cc8e00428 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:42:25 +0100 Subject: [PATCH 73/94] [deps] Billing: Update swashbuckle-aspnetcore monorepo to v7 (#5069) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- src/Api/Api.csproj | 2 +- src/SharedWeb/SharedWeb.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index d56bb2796f..f42f226153 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "swashbuckle.aspnetcore.cli": { - "version": "6.9.0", + "version": "7.2.0", "commands": ["swagger"] }, "dotnet-ef": { diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 4a018b2198..c490e90150 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -34,7 +34,7 @@ - + diff --git a/src/SharedWeb/SharedWeb.csproj b/src/SharedWeb/SharedWeb.csproj index 8d1097eeec..6df65b2310 100644 --- a/src/SharedWeb/SharedWeb.csproj +++ b/src/SharedWeb/SharedWeb.csproj @@ -7,7 +7,7 @@ - + From 11bdb93d1e1822bb518b224a141eb38c882ab048 Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Fri, 13 Dec 2024 09:41:17 -0500 Subject: [PATCH 74/94] Sign main branch container builds with cosign (#5148) * Sign main branch container builds with cosign * Properly label --- .github/workflows/build.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0fa03312b8..85c24b88c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -131,6 +131,7 @@ jobs: runs-on: ubuntu-22.04 permissions: security-events: write + id-token: write needs: - build-artifacts strategy: @@ -276,6 +277,7 @@ jobs: -d ${{ matrix.base_path }}/${{ matrix.project_name }}/obj/build-output/publish - name: Build Docker image + id: build-docker uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 with: context: ${{ matrix.base_path }}/${{ matrix.project_name }} @@ -286,6 +288,22 @@ jobs: secrets: | "GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}" + - name: Install Cosign + if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' + uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 + + - name: Sign images with Cosign + if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' + env: + DIGEST: ${{ steps.build-docker.outputs.digest }} + TAGS: ${{ steps.image-tags.outputs.tags }} + run: | + images="" + for tag in ${TAGS}; do + images+="${tag}@${DIGEST} " + done + cosign sign --yes ${images} + - name: Scan Docker image id: container-scan uses: anchore/scan-action@5ed195cc06065322983cae4bb31e2a751feb86fd # v5.2.0 From c0a9c55891dd3192568cbda4d05885f9e2f21b81 Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Fri, 13 Dec 2024 10:26:45 -0500 Subject: [PATCH 75/94] Fix image path formation for Cosign (#5151) --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85c24b88c1..420b9b6375 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -292,14 +292,15 @@ jobs: if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 - - name: Sign images with Cosign + - name: Sign image with Cosign if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' env: DIGEST: ${{ steps.build-docker.outputs.digest }} TAGS: ${{ steps.image-tags.outputs.tags }} run: | + IFS="," read -a tags <<< "${TAGS}" images="" - for tag in ${TAGS}; do + for tag in "${tags[@]}"; do images+="${tag}@${DIGEST} " done cosign sign --yes ${images} From 141a046a287a31c0d397f77981dfb9a8e77f6754 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Fri, 13 Dec 2024 14:50:20 -0500 Subject: [PATCH 76/94] [PM-14377] Add PATCH complete endpoint (#5100) * Added CQRS pattern * Added the GetManyByUserIdAsync signature to the repositiory * Added sql sproc Created user defined type to hold status Created migration file * Added ef core query * Added absract and concrete implementation for GetManyByUserIdStatusAsync * Added integration tests * Updated params to status * Implemented new query to utilize repository method * Added controller for the security task endpoint * Fixed lint issues * Added documentation * simplified to require single status modified script to check for users with edit rights * Updated ef core query * Added new assertions * simplified to require single status * fixed formatting * Fixed sql script * Removed default null * Added OperationAuthorizationRequirement for secruity task * Added and registered MarkTaskAsCompletedCommand * Added unit tests for the command * Added complete endpoint * removed false value --- .../Controllers/SecurityTaskController.cs | 19 ++++- .../Authorization/SecurityTaskOperations.cs | 16 ++++ .../Interfaces/IMarkTaskAsCompleteCommand.cs | 11 +++ .../Commands/MarkTaskAsCompletedCommand.cs | 50 +++++++++++ .../Vault/VaultServiceCollectionExtensions.cs | 5 +- .../Vault/AutoFixture/SecurityTaskFixtures.cs | 25 ++++++ .../MarkTaskAsCompletedCommandTest.cs | 83 +++++++++++++++++++ 7 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 src/Core/Vault/Authorization/SecurityTaskOperations.cs create mode 100644 src/Core/Vault/Commands/Interfaces/IMarkTaskAsCompleteCommand.cs create mode 100644 src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs create mode 100644 test/Core.Test/Vault/AutoFixture/SecurityTaskFixtures.cs create mode 100644 test/Core.Test/Vault/Commands/MarkTaskAsCompletedCommandTest.cs diff --git a/src/Api/Vault/Controllers/SecurityTaskController.cs b/src/Api/Vault/Controllers/SecurityTaskController.cs index 7b0bfa0bfb..a0b18cb847 100644 --- a/src/Api/Vault/Controllers/SecurityTaskController.cs +++ b/src/Api/Vault/Controllers/SecurityTaskController.cs @@ -3,6 +3,7 @@ using Bit.Api.Vault.Models.Response; using Bit.Core; using Bit.Core.Services; using Bit.Core.Utilities; +using Bit.Core.Vault.Commands.Interfaces; using Bit.Core.Vault.Enums; using Bit.Core.Vault.Queries; using Microsoft.AspNetCore.Authorization; @@ -17,11 +18,16 @@ public class SecurityTaskController : Controller { private readonly IUserService _userService; private readonly IGetTaskDetailsForUserQuery _getTaskDetailsForUserQuery; + private readonly IMarkTaskAsCompleteCommand _markTaskAsCompleteCommand; - public SecurityTaskController(IUserService userService, IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery) + public SecurityTaskController( + IUserService userService, + IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery, + IMarkTaskAsCompleteCommand markTaskAsCompleteCommand) { _userService = userService; _getTaskDetailsForUserQuery = getTaskDetailsForUserQuery; + _markTaskAsCompleteCommand = markTaskAsCompleteCommand; } /// @@ -37,4 +43,15 @@ public class SecurityTaskController : Controller var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList(); return new ListResponseModel(response); } + + /// + /// Marks a task as complete. The user must have edit permission on the cipher associated with the task. + /// + /// The unique identifier of the task to complete + [HttpPatch("{taskId:guid}/complete")] + public async Task Complete(Guid taskId) + { + await _markTaskAsCompleteCommand.CompleteAsync(taskId); + return NoContent(); + } } diff --git a/src/Core/Vault/Authorization/SecurityTaskOperations.cs b/src/Core/Vault/Authorization/SecurityTaskOperations.cs new file mode 100644 index 0000000000..77b504723f --- /dev/null +++ b/src/Core/Vault/Authorization/SecurityTaskOperations.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Core.Vault.Authorization; + +public class SecurityTaskOperationRequirement : OperationAuthorizationRequirement +{ + public SecurityTaskOperationRequirement(string name) + { + Name = name; + } +} + +public static class SecurityTaskOperations +{ + public static readonly SecurityTaskOperationRequirement Update = new(nameof(Update)); +} diff --git a/src/Core/Vault/Commands/Interfaces/IMarkTaskAsCompleteCommand.cs b/src/Core/Vault/Commands/Interfaces/IMarkTaskAsCompleteCommand.cs new file mode 100644 index 0000000000..1b745b8d07 --- /dev/null +++ b/src/Core/Vault/Commands/Interfaces/IMarkTaskAsCompleteCommand.cs @@ -0,0 +1,11 @@ +namespace Bit.Core.Vault.Commands.Interfaces; + +public interface IMarkTaskAsCompleteCommand +{ + /// + /// Marks a task as complete. + /// + /// The unique identifier of the task to complete + /// A task representing the async operation + Task CompleteAsync(Guid taskId); +} diff --git a/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs b/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs new file mode 100644 index 0000000000..b46fb0cecb --- /dev/null +++ b/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs @@ -0,0 +1,50 @@ +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Utilities; +using Bit.Core.Vault.Authorization; +using Bit.Core.Vault.Commands.Interfaces; +using Bit.Core.Vault.Enums; +using Bit.Core.Vault.Repositories; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.Vault.Commands; + +public class MarkTaskAsCompletedCommand : IMarkTaskAsCompleteCommand +{ + private readonly ISecurityTaskRepository _securityTaskRepository; + private readonly IAuthorizationService _authorizationService; + private readonly ICurrentContext _currentContext; + + public MarkTaskAsCompletedCommand( + ISecurityTaskRepository securityTaskRepository, + IAuthorizationService authorizationService, + ICurrentContext currentContext) + { + _securityTaskRepository = securityTaskRepository; + _authorizationService = authorizationService; + _currentContext = currentContext; + } + + /// + public async Task CompleteAsync(Guid taskId) + { + if (!_currentContext.UserId.HasValue) + { + throw new NotFoundException(); + } + + var task = await _securityTaskRepository.GetByIdAsync(taskId); + if (task is null) + { + throw new NotFoundException(); + } + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, task, + SecurityTaskOperations.Update); + + task.Status = SecurityTaskStatus.Completed; + task.RevisionDate = DateTime.UtcNow; + + await _securityTaskRepository.ReplaceAsync(task); + } +} diff --git a/src/Core/Vault/VaultServiceCollectionExtensions.cs b/src/Core/Vault/VaultServiceCollectionExtensions.cs index d3c9dd9648..15cb01f1a0 100644 --- a/src/Core/Vault/VaultServiceCollectionExtensions.cs +++ b/src/Core/Vault/VaultServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ -using Bit.Core.Vault.Queries; +using Bit.Core.Vault.Commands; +using Bit.Core.Vault.Commands.Interfaces; +using Bit.Core.Vault.Queries; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Vault; @@ -16,5 +18,6 @@ public static class VaultServiceCollectionExtensions { services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } diff --git a/test/Core.Test/Vault/AutoFixture/SecurityTaskFixtures.cs b/test/Core.Test/Vault/AutoFixture/SecurityTaskFixtures.cs new file mode 100644 index 0000000000..eb0a29421a --- /dev/null +++ b/test/Core.Test/Vault/AutoFixture/SecurityTaskFixtures.cs @@ -0,0 +1,25 @@ +using AutoFixture; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Core.Test.Vault.AutoFixture; + +public class SecurityTaskFixtures : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customize(composer => + composer + .With(task => task.Id, Guid.NewGuid()) + .With(task => task.OrganizationId, Guid.NewGuid()) + .With(task => task.Status, SecurityTaskStatus.Pending) + .Without(x => x.CipherId) + ); + } +} + +public class SecurityTaskCustomizeAttribute : BitCustomizeAttribute +{ + public override ICustomization GetCustomization() => new SecurityTaskFixtures(); +} diff --git a/test/Core.Test/Vault/Commands/MarkTaskAsCompletedCommandTest.cs b/test/Core.Test/Vault/Commands/MarkTaskAsCompletedCommandTest.cs new file mode 100644 index 0000000000..82550df48d --- /dev/null +++ b/test/Core.Test/Vault/Commands/MarkTaskAsCompletedCommandTest.cs @@ -0,0 +1,83 @@ +#nullable enable +using System.Security.Claims; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Test.Vault.AutoFixture; +using Bit.Core.Vault.Authorization; +using Bit.Core.Vault.Commands; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Vault.Commands; + +[SutProviderCustomize] +[SecurityTaskCustomize] +public class MarkTaskAsCompletedCommandTest +{ + private static void Setup(SutProvider sutProvider, Guid taskId, SecurityTask? securityTask, Guid? userId, bool authorizedUpdate = false) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency() + .GetByIdAsync(taskId) + .Returns(securityTask); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), securityTask ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(SecurityTaskOperations.Update))) + .Returns(authorizedUpdate ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + } + + [Theory] + [BitAutoData] + public async Task CompleteAsync_NotLoggedIn_NotFoundException( + SutProvider sutProvider, + Guid taskId, + SecurityTask securityTask) + { + Setup(sutProvider, taskId, securityTask, null, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CompleteAsync(taskId)); + } + + [Theory] + [BitAutoData] + public async Task CompleteAsync_TaskNotFound_NotFoundException( + SutProvider sutProvider, + Guid taskId) + { + Setup(sutProvider, taskId, null, Guid.NewGuid(), true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CompleteAsync(taskId)); + } + + [Theory] + [BitAutoData] + public async Task CompleteAsync_AuthorizationFailed_NotFoundException( + SutProvider sutProvider, + Guid taskId, + SecurityTask securityTask) + { + Setup(sutProvider, taskId, securityTask, Guid.NewGuid()); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CompleteAsync(taskId)); + } + + [Theory] + [BitAutoData] + public async Task CompleteAsync_Success( + SutProvider sutProvider, + Guid taskId, + SecurityTask securityTask) + { + Setup(sutProvider, taskId, securityTask, Guid.NewGuid(), true); + + await sutProvider.Sut.CompleteAsync(taskId); + + await sutProvider.GetDependency().Received(1).ReplaceAsync(securityTask); + } +} From a8091bf58539737c506162643a2e06463376d917 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 13 Dec 2024 16:04:55 -0500 Subject: [PATCH 77/94] chore(db): add `Installation.LastActivityDate` column (#5060) * chore(mssql): add `Installation.LastActivityDate` column * chore(ef): add `Installation.LastActivityDate` column --- .../Models/Installation.cs | 8 + .../Stored Procedures/Installation_Create.sql | 11 +- .../Stored Procedures/Installation_Update.sql | 10 +- src/Sql/dbo/Tables/Installation.sql | 13 +- ..._AddInstallationLastActivityDateColumn.sql | 72 + ...allationLastActivityDateColumn.Designer.cs | 2943 ++++++++++++++++ ...8_AddInstallationLastActivityDateColumn.cs | 27 + .../DatabaseContextModelSnapshot.cs | 3 + ...allationLastActivityDateColumn.Designer.cs | 2949 +++++++++++++++++ ...3_AddInstallationLastActivityDateColumn.cs | 27 + .../DatabaseContextModelSnapshot.cs | 3 + ...allationLastActivityDateColumn.Designer.cs | 2932 ++++++++++++++++ ...2_AddInstallationLastActivityDateColumn.cs | 27 + .../DatabaseContextModelSnapshot.cs | 3 + 14 files changed, 9014 insertions(+), 14 deletions(-) create mode 100644 util/Migrator/DbScripts/2024-12-02_00_AddInstallationLastActivityDateColumn.sql create mode 100644 util/MySqlMigrations/Migrations/20241202201938_AddInstallationLastActivityDateColumn.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20241202201938_AddInstallationLastActivityDateColumn.cs create mode 100644 util/PostgresMigrations/Migrations/20241202201943_AddInstallationLastActivityDateColumn.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20241202201943_AddInstallationLastActivityDateColumn.cs create mode 100644 util/SqliteMigrations/Migrations/20241202201932_AddInstallationLastActivityDateColumn.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20241202201932_AddInstallationLastActivityDateColumn.cs diff --git a/src/Infrastructure.EntityFramework/Models/Installation.cs b/src/Infrastructure.EntityFramework/Models/Installation.cs index 35223a33d7..c38680a23c 100644 --- a/src/Infrastructure.EntityFramework/Models/Installation.cs +++ b/src/Infrastructure.EntityFramework/Models/Installation.cs @@ -4,12 +4,20 @@ namespace Bit.Infrastructure.EntityFramework.Models; public class Installation : Core.Entities.Installation { + // Shadow property - to be introduced by https://bitwarden.atlassian.net/browse/PM-11129 + // This isn't a value or entity used by self hosted servers, but it's + // being added for synchronicity between database provider options. + public DateTime? LastActivityDate { get; set; } } public class InstallationMapperProfile : Profile { public InstallationMapperProfile() { + CreateMap() + // Shadow property - to be introduced by https://bitwarden.atlassian.net/browse/PM-11129 + .ForMember(i => i.LastActivityDate, opt => opt.Ignore()) + .ReverseMap(); CreateMap().ReverseMap(); } } diff --git a/src/Sql/dbo/Stored Procedures/Installation_Create.sql b/src/Sql/dbo/Stored Procedures/Installation_Create.sql index 8c91a5b81a..40a6ea069f 100644 --- a/src/Sql/dbo/Stored Procedures/Installation_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Installation_Create.sql @@ -1,9 +1,10 @@ -CREATE PROCEDURE [dbo].[Installation_Create] +CREATE PROCEDURE [dbo].[Installation_Create] @Id UNIQUEIDENTIFIER OUTPUT, @Email NVARCHAR(256), @Key VARCHAR(150), @Enabled BIT, - @CreationDate DATETIME2(7) + @CreationDate DATETIME2(7), + @LastActivityDate DATETIME2(7) = NULL AS BEGIN SET NOCOUNT ON @@ -14,7 +15,8 @@ BEGIN [Email], [Key], [Enabled], - [CreationDate] + [CreationDate], + [LastActivityDate] ) VALUES ( @@ -22,6 +24,7 @@ BEGIN @Email, @Key, @Enabled, - @CreationDate + @CreationDate, + @LastActivityDate ) END diff --git a/src/Sql/dbo/Stored Procedures/Installation_Update.sql b/src/Sql/dbo/Stored Procedures/Installation_Update.sql index af2fd8737c..51ef47bfab 100644 --- a/src/Sql/dbo/Stored Procedures/Installation_Update.sql +++ b/src/Sql/dbo/Stored Procedures/Installation_Update.sql @@ -1,9 +1,10 @@ -CREATE PROCEDURE [dbo].[Installation_Update] +CREATE PROCEDURE [dbo].[Installation_Update] @Id UNIQUEIDENTIFIER, @Email NVARCHAR(256), @Key VARCHAR(150), @Enabled BIT, - @CreationDate DATETIME2(7) + @CreationDate DATETIME2(7), + @LastActivityDate DATETIME2(7) = NULL AS BEGIN SET NOCOUNT ON @@ -14,7 +15,8 @@ BEGIN [Email] = @Email, [Key] = @Key, [Enabled] = @Enabled, - [CreationDate] = @CreationDate + [CreationDate] = @CreationDate, + [LastActivityDate] = @LastActivityDate WHERE [Id] = @Id -END \ No newline at end of file +END diff --git a/src/Sql/dbo/Tables/Installation.sql b/src/Sql/dbo/Tables/Installation.sql index df4b7260ed..207e94a569 100644 --- a/src/Sql/dbo/Tables/Installation.sql +++ b/src/Sql/dbo/Tables/Installation.sql @@ -1,9 +1,10 @@ -CREATE TABLE [dbo].[Installation] ( - [Id] UNIQUEIDENTIFIER NOT NULL, - [Email] NVARCHAR (256) NOT NULL, - [Key] VARCHAR (150) NOT NULL, - [Enabled] BIT NOT NULL, - [CreationDate] DATETIME2 (7) NOT NULL, +CREATE TABLE [dbo].[Installation] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [Email] NVARCHAR (256) NOT NULL, + [Key] VARCHAR (150) NOT NULL, + [Enabled] BIT NOT NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [LastActivityDate] DATETIME2 (7) NULL, CONSTRAINT [PK_Installation] PRIMARY KEY CLUSTERED ([Id] ASC) ); diff --git a/util/Migrator/DbScripts/2024-12-02_00_AddInstallationLastActivityDateColumn.sql b/util/Migrator/DbScripts/2024-12-02_00_AddInstallationLastActivityDateColumn.sql new file mode 100644 index 0000000000..3023c6e2d9 --- /dev/null +++ b/util/Migrator/DbScripts/2024-12-02_00_AddInstallationLastActivityDateColumn.sql @@ -0,0 +1,72 @@ +IF COL_LENGTH('[dbo].[Installation]', 'LastActivityDate') IS NULL +BEGIN + ALTER TABLE + [dbo].[Installation] + ADD + [LastActivityDate] DATETIME2 (7) NULL +END +GO + +CREATE OR ALTER VIEW [dbo].[InstallationView] +AS + SELECT + * + FROM + [dbo].[Installation] +GO + +CREATE OR ALTER PROCEDURE [dbo].[Installation_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @Email NVARCHAR(256), + @Key VARCHAR(150), + @Enabled BIT, + @CreationDate DATETIME2(7), + @LastActivityDate DATETIME2(7) = NULL +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Installation] + ( + [Id], + [Email], + [Key], + [Enabled], + [CreationDate], + [LastActivityDate] + ) + VALUES + ( + @Id, + @Email, + @Key, + @Enabled, + @CreationDate, + @LastActivityDate + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Installation_Update] + @Id UNIQUEIDENTIFIER, + @Email NVARCHAR(256), + @Key VARCHAR(150), + @Enabled BIT, + @CreationDate DATETIME2(7), + @LastActivityDate DATETIME2(7) = NULL +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Installation] + SET + [Email] = @Email, + [Key] = @Key, + [Enabled] = @Enabled, + [CreationDate] = @CreationDate, + [LastActivityDate] = @LastActivityDate + WHERE + [Id] = @Id +END +GO diff --git a/util/MySqlMigrations/Migrations/20241202201938_AddInstallationLastActivityDateColumn.Designer.cs b/util/MySqlMigrations/Migrations/20241202201938_AddInstallationLastActivityDateColumn.Designer.cs new file mode 100644 index 0000000000..ff37c4716c --- /dev/null +++ b/util/MySqlMigrations/Migrations/20241202201938_AddInstallationLastActivityDateColumn.Designer.cs @@ -0,0 +1,2943 @@ +// +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("20241202201938_AddInstallationLastActivityDateColumn")] + partial class AddInstallationLastActivityDateColumn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionCreationDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasColumnType("longtext"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AaGuid") + .HasColumnType("char(36)"); + + b.Property("Counter") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasColumnType("longtext"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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.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.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + 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/20241202201938_AddInstallationLastActivityDateColumn.cs b/util/MySqlMigrations/Migrations/20241202201938_AddInstallationLastActivityDateColumn.cs new file mode 100644 index 0000000000..aecdd01f95 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20241202201938_AddInstallationLastActivityDateColumn.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddInstallationLastActivityDateColumn : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastActivityDate", + table: "Installation", + type: "datetime(6)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastActivityDate", + table: "Installation"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 000274387c..ed26d612a2 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1165,6 +1165,9 @@ namespace Bit.MySqlMigrations.Migrations .HasMaxLength(150) .HasColumnType("varchar(150)"); + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + b.HasKey("Id"); b.ToTable("Installation", (string)null); diff --git a/util/PostgresMigrations/Migrations/20241202201943_AddInstallationLastActivityDateColumn.Designer.cs b/util/PostgresMigrations/Migrations/20241202201943_AddInstallationLastActivityDateColumn.Designer.cs new file mode 100644 index 0000000000..6bb6395156 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20241202201943_AddInstallationLastActivityDateColumn.Designer.cs @@ -0,0 +1,2949 @@ +// +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("20241202201943_AddInstallationLastActivityDateColumn")] + partial class AddInstallationLastActivityDateColumn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionCreationDeletion") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasColumnType("text"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MasterPasswordHash") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("OrganizationId", "ExternalId"), new[] { "UserId" }); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("Counter") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsPrf") + .HasColumnType("boolean"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasColumnType("text"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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.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.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + 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/20241202201943_AddInstallationLastActivityDateColumn.cs b/util/PostgresMigrations/Migrations/20241202201943_AddInstallationLastActivityDateColumn.cs new file mode 100644 index 0000000000..88a93ee851 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20241202201943_AddInstallationLastActivityDateColumn.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddInstallationLastActivityDateColumn : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastActivityDate", + table: "Installation", + type: "timestamp with time zone", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastActivityDate", + table: "Installation"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index c8cce33e11..04636ab15d 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1170,6 +1170,9 @@ namespace Bit.PostgresMigrations.Migrations .HasMaxLength(150) .HasColumnType("character varying(150)"); + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + b.HasKey("Id"); b.ToTable("Installation", (string)null); diff --git a/util/SqliteMigrations/Migrations/20241202201932_AddInstallationLastActivityDateColumn.Designer.cs b/util/SqliteMigrations/Migrations/20241202201932_AddInstallationLastActivityDateColumn.Designer.cs new file mode 100644 index 0000000000..a810146726 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20241202201932_AddInstallationLastActivityDateColumn.Designer.cs @@ -0,0 +1,2932 @@ +// +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("20241202201932_AddInstallationLastActivityDateColumn")] + partial class AddInstallationLastActivityDateColumn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionCreationDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResponseDate") + .HasColumnType("TEXT"); + + b.Property("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GranteeId") + .HasColumnType("TEXT"); + + b.Property("GrantorId") + .HasColumnType("TEXT"); + + b.Property("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WaitTimeDays") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AaGuid") + .HasColumnType("TEXT"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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.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.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + 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/20241202201932_AddInstallationLastActivityDateColumn.cs b/util/SqliteMigrations/Migrations/20241202201932_AddInstallationLastActivityDateColumn.cs new file mode 100644 index 0000000000..9238326096 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20241202201932_AddInstallationLastActivityDateColumn.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddInstallationLastActivityDateColumn : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastActivityDate", + table: "Installation", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastActivityDate", + table: "Installation"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 12a1c9792b..d813ebcbcc 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1154,6 +1154,9 @@ namespace Bit.SqliteMigrations.Migrations .HasMaxLength(150) .HasColumnType("TEXT"); + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + b.HasKey("Id"); b.ToTable("Installation", (string)null); From 9321515eca750c8f9bc84126912827ed11d36aa7 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Mon, 16 Dec 2024 08:04:05 -0500 Subject: [PATCH 78/94] [PM-10873] Updated errors thrown when creating organization on selfhost to be more specific (#5007) * Updated errors thrown when creating organization on selfhost to be more specific * Added additional validation to ensure that the license type is accurate --------- Co-authored-by: Matt Bishop --- .../Implementations/OrganizationService.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 49f339cc9a..1cf22b23ad 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -519,17 +519,29 @@ public class OrganizationService : IOrganizationService OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey, string privateKey) { + if (license.LicenseType != LicenseType.Organization) + { + throw new BadRequestException("Premium licenses cannot be applied to an organization. " + + "Upload this license from your personal account settings page."); + } + var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license); var canUse = license.CanUse(_globalSettings, _licensingService, claimsPrincipal, out var exception); + if (!canUse) { throw new BadRequestException(exception); } - if (license.PlanType != PlanType.Custom && - StaticStore.Plans.FirstOrDefault(p => p.Type == license.PlanType && !p.Disabled) == null) + var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == license.PlanType); + if (plan is null) { - throw new BadRequestException("Plan not found."); + throw new BadRequestException($"Server must be updated to support {license.Plan}."); + } + + if (license.PlanType != PlanType.Custom && plan.Disabled) + { + throw new BadRequestException($"Plan {plan.Name} is disabled."); } var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync(); From d0c72a34f12baadede2c191a814315f153872b4a Mon Sep 17 00:00:00 2001 From: Opeyemi Date: Mon, 16 Dec 2024 14:21:05 +0000 Subject: [PATCH 79/94] Update SH Unified Build trigger (#5154) * Update SH Unified Build trigger * make value a boolean --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 420b9b6375..eb644fe8b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -580,6 +580,7 @@ jobs: ref: 'main', inputs: { server_branch: process.env.GITHUB_REF + is_workflow_call: true } }); From 8994d1d7dd019e857d68e8ae46262f8daea82058 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:11:56 +0000 Subject: [PATCH 80/94] [deps] Tools: Update aws-sdk-net monorepo (#5126) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- src/Core/Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index fd4d8cc7e1..9049f94dcf 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,8 +21,8 @@ - - + + From c446ac86fead55e6d7b762ca44ea814d9d53e80b Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Mon, 16 Dec 2024 07:57:56 -0800 Subject: [PATCH 81/94] [PM-12512] Add Endpoint to allow users to request a new device otp (#5146) feat(NewDeviceVerification): Added a resend new device OTP endpoint and method for the IUserService as well as wrote test for new methods for the user service. --- .../Auth/Controllers/AccountsController.cs | 8 + ...henticatedSecretVerificatioRequestModel.cs | 12 ++ src/Core/Services/IUserService.cs | 2 +- .../Services/Implementations/UserService.cs | 16 +- test/Core.Test/Services/UserServiceTests.cs | 171 ++++++++++++++---- 5 files changed, 171 insertions(+), 38 deletions(-) create mode 100644 src/Api/Auth/Models/Request/Accounts/UnauthenticatedSecretVerificatioRequestModel.cs diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index a94e170cbb..1c08ce4f73 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -961,6 +961,14 @@ public class AccountsController : Controller } } + [RequireFeature(FeatureFlagKeys.NewDeviceVerification)] + [AllowAnonymous] + [HttpPost("resend-new-device-otp")] + public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificatioRequestModel request) + { + await _userService.ResendNewDeviceVerificationEmail(request.Email, request.Secret); + } + private async Task> GetOrganizationIdsManagingUserAsync(Guid userId) { var organizationManagingUser = await _userService.GetOrganizationsManagingUserAsync(userId); diff --git a/src/Api/Auth/Models/Request/Accounts/UnauthenticatedSecretVerificatioRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/UnauthenticatedSecretVerificatioRequestModel.cs new file mode 100644 index 0000000000..629896b8c4 --- /dev/null +++ b/src/Api/Auth/Models/Request/Accounts/UnauthenticatedSecretVerificatioRequestModel.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Utilities; + +namespace Bit.Api.Auth.Models.Request.Accounts; + +public class UnauthenticatedSecretVerificatioRequestModel : SecretVerificationRequestModel +{ + [Required] + [StrictEmailAddress] + [StringLength(256)] + public string Email { get; set; } +} diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 65bec5ea9f..f0ba535266 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -76,7 +76,7 @@ public interface IUserService Task SendOTPAsync(User user); Task VerifyOTPAsync(User user, string token); Task VerifySecretAsync(User user, string secret, bool isSettingMFA = false); - + Task ResendNewDeviceVerificationEmail(string email, string secret); void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool setEnabled = true); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 2bc81959b9..cb17d6e26b 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1407,7 +1407,7 @@ public class UserService : UserManager, IUserService, IDisposable public async Task SendOTPAsync(User user) { - if (user.Email == null) + if (string.IsNullOrEmpty(user.Email)) { throw new BadRequestException("No user email."); } @@ -1450,6 +1450,20 @@ public class UserService : UserManager, IUserService, IDisposable return isVerified; } + public async Task ResendNewDeviceVerificationEmail(string email, string secret) + { + var user = await _userRepository.GetByEmailAsync(email); + if (user == null) + { + return; + } + + if (await VerifySecretAsync(user, secret)) + { + await SendOTPAsync(user); + } + } + private async Task SendAppropriateWelcomeEmailAsync(User user, string initiationPath) { var isFromMarketingWebsite = initiationPath.Contains("Secrets Manager trial"); diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index de2a518717..e44609c6d6 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -13,6 +13,7 @@ using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; @@ -240,42 +241,7 @@ public class UserServiceTests }); // HACK: SutProvider is being weird about not injecting the IPasswordHasher that I configured - var sut = new UserService( - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency>(), - sutProvider.GetDependency>(), - sutProvider.GetDependency>(), - sutProvider.GetDependency>>(), - sutProvider.GetDependency>>(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency>>(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - new FakeDataProtectorTokenFactory(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency(), - sutProvider.GetDependency() - ); + var sut = RebuildSut(sutProvider); var actualIsVerified = await sut.VerifySecretAsync(user, secret); @@ -522,6 +488,99 @@ public class UserServiceTests .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization2.DisplayName(), user.Email); } + [Theory, BitAutoData] + public async Task ResendNewDeviceVerificationEmail_UserNull_SendOTPAsyncNotCalled( + SutProvider sutProvider, string email, string secret) + { + sutProvider.GetDependency() + .GetByEmailAsync(email) + .Returns(null as User); + + await sutProvider.Sut.ResendNewDeviceVerificationEmail(email, secret); + + await sutProvider.GetDependency() + .DidNotReceive() + .SendOTPEmailAsync(Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ResendNewDeviceVerificationEmail_SecretNotValid_SendOTPAsyncNotCalled( + SutProvider sutProvider, string email, string secret) + { + sutProvider.GetDependency() + .GetByEmailAsync(email) + .Returns(null as User); + + await sutProvider.Sut.ResendNewDeviceVerificationEmail(email, secret); + + await sutProvider.GetDependency() + .DidNotReceive() + .SendOTPEmailAsync(Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ResendNewDeviceVerificationEmail_SendsToken_Success( + SutProvider sutProvider, User user) + { + // Arrange + var testPassword = "test_password"; + var tokenProvider = SetupFakeTokenProvider(sutProvider, user); + SetupUserAndDevice(user, true); + + // Setup the fake password verification + var substitutedUserPasswordStore = Substitute.For>(); + substitutedUserPasswordStore + .GetPasswordHashAsync(user, Arg.Any()) + .Returns((ci) => + { + return Task.FromResult("hashed_test_password"); + }); + + sutProvider.SetDependency>(substitutedUserPasswordStore, "store"); + + sutProvider.GetDependency>("passwordHasher") + .VerifyHashedPassword(user, "hashed_test_password", testPassword) + .Returns((ci) => + { + return PasswordVerificationResult.Success; + }); + + sutProvider.GetDependency() + .GetByEmailAsync(user.Email) + .Returns(user); + + // HACK: SutProvider is being weird about not injecting the IPasswordHasher that I configured + var sut = RebuildSut(sutProvider); + + await sut.ResendNewDeviceVerificationEmail(user.Email, testPassword); + + await sutProvider.GetDependency() + .Received(1) + .SendOTPEmailAsync(user.Email, Arg.Any()); + } + + [Theory] + [BitAutoData("")] + [BitAutoData("null")] + public async Task SendOTPAsync_UserEmailNull_ThrowsBadRequest( + string email, + SutProvider sutProvider, User user) + { + user.Email = email == "null" ? null : ""; + var expectedMessage = "No user email."; + try + { + await sutProvider.Sut.SendOTPAsync(user); + } + catch (BadRequestException ex) + { + Assert.Equal(ex.Message, expectedMessage); + await sutProvider.GetDependency() + .DidNotReceive() + .SendOTPEmailAsync(Arg.Any(), Arg.Any()); + } + } + private static void SetupUserAndDevice(User user, bool shouldHavePassword) { @@ -573,4 +632,44 @@ public class UserServiceTests return fakeUserTwoFactorProvider; } + + private IUserService RebuildSut(SutProvider sutProvider) + { + return new UserService( + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency>(), + sutProvider.GetDependency>(), + sutProvider.GetDependency>(), + sutProvider.GetDependency>>(), + sutProvider.GetDependency>>(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency>>(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + new FakeDataProtectorTokenFactory(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency() + ); + } } From d88a103fbc8baa60147eaf377d05eeeb87b1eb7a Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:11:37 +0100 Subject: [PATCH 82/94] Move CSVHelper under billing ownership (#5156) Co-authored-by: Daniel James Smith --- .github/renovate.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/renovate.json b/.github/renovate.json index 5779b28edb..4ae3cc19d8 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -63,6 +63,7 @@ "BitPay.Light", "Braintree", "coverlet.collector", + "CsvHelper", "FluentAssertions", "Kralizek.AutoFixture.Extensions.MockHttp", "Microsoft.AspNetCore.Mvc.Testing", From 7637cbe12ac67006db73573f3eb9a1010a7cb153 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:01:09 -0600 Subject: [PATCH 83/94] [PM-13362] Add private key regeneration endpoint (#4929) * Add new RegenerateUserAsymmetricKeysCommand * add new command tests * Add regen controller * Add regen controller tests * add feature flag * Add push notification to sync new asymmetric keys to other devices --- .../AccountsKeyManagementController.cs | 50 +++++ .../Requests/KeyRegenerationRequestModel.cs | 23 ++ src/Core/Constants.cs | 1 + .../IRegenerateUserAsymmetricKeysCommand.cs | 13 ++ .../RegenerateUserAsymmetricKeysCommand.cs | 71 +++++++ ...eyManagementServiceCollectionExtensions.cs | 18 ++ .../Utilities/ServiceCollectionExtensions.cs | 2 + .../Helpers/LoginHelper.cs | 6 + .../AccountsKeyManagementControllerTests.cs | 164 +++++++++++++++ .../AccountsKeyManagementControllerTests.cs | 96 +++++++++ ...egenerateUserAsymmetricKeysCommandTests.cs | 197 ++++++++++++++++++ 11 files changed, 641 insertions(+) create mode 100644 src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs create mode 100644 src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs create mode 100644 src/Core/KeyManagement/Commands/Interfaces/IRegenerateUserAsymmetricKeysCommand.cs create mode 100644 src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs create mode 100644 src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs create mode 100644 test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs create mode 100644 test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs create mode 100644 test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs diff --git a/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs new file mode 100644 index 0000000000..b8d5e30949 --- /dev/null +++ b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs @@ -0,0 +1,50 @@ +#nullable enable +using Bit.Api.KeyManagement.Models.Requests; +using Bit.Core; +using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Commands.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.KeyManagement.Controllers; + +[Route("accounts/key-management")] +[Authorize("Application")] +public class AccountsKeyManagementController : Controller +{ + private readonly IEmergencyAccessRepository _emergencyAccessRepository; + private readonly IFeatureService _featureService; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IRegenerateUserAsymmetricKeysCommand _regenerateUserAsymmetricKeysCommand; + private readonly IUserService _userService; + + public AccountsKeyManagementController(IUserService userService, + IFeatureService featureService, + IOrganizationUserRepository organizationUserRepository, + IEmergencyAccessRepository emergencyAccessRepository, + IRegenerateUserAsymmetricKeysCommand regenerateUserAsymmetricKeysCommand) + { + _userService = userService; + _featureService = featureService; + _regenerateUserAsymmetricKeysCommand = regenerateUserAsymmetricKeysCommand; + _organizationUserRepository = organizationUserRepository; + _emergencyAccessRepository = emergencyAccessRepository; + } + + [HttpPost("regenerate-keys")] + public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel request) + { + if (!_featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration)) + { + throw new NotFoundException(); + } + + var user = await _userService.GetUserByPrincipalAsync(User) ?? throw new UnauthorizedAccessException(); + var usersOrganizationAccounts = await _organizationUserRepository.GetManyByUserAsync(user.Id); + var designatedEmergencyAccess = await _emergencyAccessRepository.GetManyDetailsByGranteeIdAsync(user.Id); + await _regenerateUserAsymmetricKeysCommand.RegenerateKeysAsync(request.ToUserAsymmetricKeys(user.Id), + usersOrganizationAccounts, designatedEmergencyAccess); + } +} diff --git a/src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs b/src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs new file mode 100644 index 0000000000..495d13cccd --- /dev/null +++ b/src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs @@ -0,0 +1,23 @@ +#nullable enable +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Utilities; + +namespace Bit.Api.KeyManagement.Models.Requests; + +public class KeyRegenerationRequestModel +{ + public required string UserPublicKey { get; set; } + + [EncryptedString] + public required string UserKeyEncryptedUserPrivateKey { get; set; } + + public UserAsymmetricKeys ToUserAsymmetricKeys(Guid userId) + { + return new UserAsymmetricKeys + { + UserId = userId, + PublicKey = UserPublicKey, + UserKeyEncryptedPrivateKey = UserKeyEncryptedUserPrivateKey, + }; + } +} diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index df0abfb4b9..2c315b2578 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -160,6 +160,7 @@ public static class FeatureFlagKeys public const string PM12443RemovePagingLogic = "pm-12443-remove-paging-logic"; public const string SelfHostLicenseRefactor = "pm-11516-self-host-license-refactor"; public const string PromoteProviderServiceUserTool = "pm-15128-promote-provider-service-user-tool"; + public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration"; public static List GetAllKeys() { diff --git a/src/Core/KeyManagement/Commands/Interfaces/IRegenerateUserAsymmetricKeysCommand.cs b/src/Core/KeyManagement/Commands/Interfaces/IRegenerateUserAsymmetricKeysCommand.cs new file mode 100644 index 0000000000..d7ad7e3959 --- /dev/null +++ b/src/Core/KeyManagement/Commands/Interfaces/IRegenerateUserAsymmetricKeysCommand.cs @@ -0,0 +1,13 @@ +#nullable enable +using Bit.Core.Auth.Models.Data; +using Bit.Core.Entities; +using Bit.Core.KeyManagement.Models.Data; + +namespace Bit.Core.KeyManagement.Commands.Interfaces; + +public interface IRegenerateUserAsymmetricKeysCommand +{ + Task RegenerateKeysAsync(UserAsymmetricKeys userAsymmetricKeys, + ICollection usersOrganizationAccounts, + ICollection designatedEmergencyAccess); +} diff --git a/src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs b/src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs new file mode 100644 index 0000000000..a54223f685 --- /dev/null +++ b/src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs @@ -0,0 +1,71 @@ +#nullable enable +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Commands.Interfaces; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Repositories; +using Bit.Core.Services; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.KeyManagement.Commands; + +public class RegenerateUserAsymmetricKeysCommand : IRegenerateUserAsymmetricKeysCommand +{ + private readonly ICurrentContext _currentContext; + private readonly ILogger _logger; + private readonly IUserAsymmetricKeysRepository _userAsymmetricKeysRepository; + private readonly IPushNotificationService _pushService; + + public RegenerateUserAsymmetricKeysCommand( + ICurrentContext currentContext, + IUserAsymmetricKeysRepository userAsymmetricKeysRepository, + IPushNotificationService pushService, + ILogger logger) + { + _currentContext = currentContext; + _logger = logger; + _userAsymmetricKeysRepository = userAsymmetricKeysRepository; + _pushService = pushService; + } + + public async Task RegenerateKeysAsync(UserAsymmetricKeys userAsymmetricKeys, + ICollection usersOrganizationAccounts, + ICollection designatedEmergencyAccess) + { + var userId = _currentContext.UserId; + if (!userId.HasValue || + userAsymmetricKeys.UserId != userId.Value || + usersOrganizationAccounts.Any(ou => ou.UserId != userId) || + designatedEmergencyAccess.Any(dea => dea.GranteeId != userId)) + { + throw new NotFoundException(); + } + + var inOrganizations = usersOrganizationAccounts.Any(ou => + ou.Status is OrganizationUserStatusType.Confirmed or OrganizationUserStatusType.Revoked); + var hasDesignatedEmergencyAccess = designatedEmergencyAccess.Any(x => + x.Status is EmergencyAccessStatusType.Confirmed or EmergencyAccessStatusType.RecoveryApproved + or EmergencyAccessStatusType.RecoveryInitiated); + + _logger.LogInformation( + "User asymmetric keys regeneration requested. UserId: {userId} OrganizationMembership: {inOrganizations} DesignatedEmergencyAccess: {hasDesignatedEmergencyAccess} DeviceType: {deviceType}", + userAsymmetricKeys.UserId, inOrganizations, hasDesignatedEmergencyAccess, _currentContext.DeviceType); + + // For now, don't regenerate asymmetric keys for user's with organization membership and designated emergency access. + if (inOrganizations || hasDesignatedEmergencyAccess) + { + throw new BadRequestException("Key regeneration not supported for this user."); + } + + await _userAsymmetricKeysRepository.RegenerateUserAsymmetricKeysAsync(userAsymmetricKeys); + _logger.LogInformation( + "User's asymmetric keys regenerated. UserId: {userId} OrganizationMembership: {inOrganizations} DesignatedEmergencyAccess: {hasDesignatedEmergencyAccess} DeviceType: {deviceType}", + userAsymmetricKeys.UserId, inOrganizations, hasDesignatedEmergencyAccess, _currentContext.DeviceType); + + await _pushService.PushSyncSettingsAsync(userId.Value); + } +} diff --git a/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs b/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs new file mode 100644 index 0000000000..102630c7e6 --- /dev/null +++ b/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs @@ -0,0 +1,18 @@ +using Bit.Core.KeyManagement.Commands; +using Bit.Core.KeyManagement.Commands.Interfaces; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.KeyManagement; + +public static class KeyManagementServiceCollectionExtensions +{ + public static void AddKeyManagementServices(this IServiceCollection services) + { + services.AddKeyManagementCommands(); + } + + private static void AddKeyManagementCommands(this IServiceCollection services) + { + services.AddScoped(); + } +} diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 7585739d82..c757f163e9 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -26,6 +26,7 @@ using Bit.Core.Enums; using Bit.Core.HostedServices; using Bit.Core.Identity; using Bit.Core.IdentityServer; +using Bit.Core.KeyManagement; using Bit.Core.NotificationHub; using Bit.Core.OrganizationFeatures; using Bit.Core.Repositories; @@ -120,6 +121,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddVaultServices(); services.AddReportingServices(); + services.AddKeyManagementServices(); } public static void AddTokenizers(this IServiceCollection services) diff --git a/test/Api.IntegrationTest/Helpers/LoginHelper.cs b/test/Api.IntegrationTest/Helpers/LoginHelper.cs index d6ce911bd0..1f5eb725d9 100644 --- a/test/Api.IntegrationTest/Helpers/LoginHelper.cs +++ b/test/Api.IntegrationTest/Helpers/LoginHelper.cs @@ -16,6 +16,12 @@ public class LoginHelper _client = client; } + public async Task LoginAsync(string email) + { + var tokens = await _factory.LoginAsync(email); + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); + } + public async Task LoginWithOrganizationApiKeyAsync(Guid organizationId) { var (clientId, apiKey) = await GetOrganizationApiKey(_factory, organizationId); diff --git a/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs b/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs new file mode 100644 index 0000000000..ec7ca37460 --- /dev/null +++ b/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -0,0 +1,164 @@ +using System.Net; +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.Helpers; +using Bit.Api.KeyManagement.Models.Requests; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Billing.Enums; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Api.IntegrationTest.KeyManagement.Controllers; + +public class AccountsKeyManagementControllerTests : IClassFixture, IAsyncLifetime +{ + private static readonly string _mockEncryptedString = + "2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk="; + + private readonly HttpClient _client; + private readonly IEmergencyAccessRepository _emergencyAccessRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly ApiApplicationFactory _factory; + private readonly LoginHelper _loginHelper; + private readonly IUserRepository _userRepository; + private string _ownerEmail = null!; + + public AccountsKeyManagementControllerTests(ApiApplicationFactory factory) + { + _factory = factory; + _factory.UpdateConfiguration("globalSettings:launchDarkly:flagValues:pm-12241-private-key-regeneration", + "true"); + _client = factory.CreateClient(); + _loginHelper = new LoginHelper(_factory, _client); + _userRepository = _factory.GetService(); + _emergencyAccessRepository = _factory.GetService(); + _organizationUserRepository = _factory.GetService(); + } + + public async Task InitializeAsync() + { + _ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(_ownerEmail); + } + + public Task DisposeAsync() + { + _client.Dispose(); + return Task.CompletedTask; + } + + [Theory] + [BitAutoData] + public async Task RegenerateKeysAsync_FeatureFlagTurnedOff_NotFound(KeyRegenerationRequestModel request) + { + // Localize factory to inject a false value for the feature flag. + var localFactory = new ApiApplicationFactory(); + localFactory.UpdateConfiguration("globalSettings:launchDarkly:flagValues:pm-12241-private-key-regeneration", + "false"); + var localClient = localFactory.CreateClient(); + var localEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + var localLoginHelper = new LoginHelper(localFactory, localClient); + await localFactory.LoginWithNewAccount(localEmail); + await localLoginHelper.LoginAsync(localEmail); + + request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString; + + var response = await localClient.PostAsJsonAsync("/accounts/key-management/regenerate-keys", request); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [BitAutoData] + public async Task RegenerateKeysAsync_NotLoggedIn_Unauthorized(KeyRegenerationRequestModel request) + { + request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString; + + var response = await _client.PostAsJsonAsync("/accounts/key-management/regenerate-keys", request); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Theory] + [BitAutoData(OrganizationUserStatusType.Confirmed, EmergencyAccessStatusType.Confirmed)] + [BitAutoData(OrganizationUserStatusType.Confirmed, EmergencyAccessStatusType.RecoveryApproved)] + [BitAutoData(OrganizationUserStatusType.Confirmed, EmergencyAccessStatusType.RecoveryInitiated)] + [BitAutoData(OrganizationUserStatusType.Revoked, EmergencyAccessStatusType.Confirmed)] + [BitAutoData(OrganizationUserStatusType.Revoked, EmergencyAccessStatusType.RecoveryApproved)] + [BitAutoData(OrganizationUserStatusType.Revoked, EmergencyAccessStatusType.RecoveryInitiated)] + [BitAutoData(OrganizationUserStatusType.Confirmed, null)] + [BitAutoData(OrganizationUserStatusType.Revoked, null)] + [BitAutoData(OrganizationUserStatusType.Invited, EmergencyAccessStatusType.Confirmed)] + [BitAutoData(OrganizationUserStatusType.Invited, EmergencyAccessStatusType.RecoveryApproved)] + [BitAutoData(OrganizationUserStatusType.Invited, EmergencyAccessStatusType.RecoveryInitiated)] + public async Task RegenerateKeysAsync_UserInOrgOrHasDesignatedEmergencyAccess_ThrowsBadRequest( + OrganizationUserStatusType organizationUserStatus, + EmergencyAccessStatusType? emergencyAccessStatus, + KeyRegenerationRequestModel request) + { + if (organizationUserStatus is OrganizationUserStatusType.Confirmed or OrganizationUserStatusType.Revoked) + { + await CreateOrganizationUserAsync(organizationUserStatus); + } + + if (emergencyAccessStatus != null) + { + await CreateDesignatedEmergencyAccessAsync(emergencyAccessStatus.Value); + } + + await _loginHelper.LoginAsync(_ownerEmail); + request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString; + + var response = await _client.PostAsJsonAsync("/accounts/key-management/regenerate-keys", request); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Theory] + [BitAutoData] + public async Task RegenerateKeysAsync_Success(KeyRegenerationRequestModel request) + { + await _loginHelper.LoginAsync(_ownerEmail); + request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString; + + var response = await _client.PostAsJsonAsync("/accounts/key-management/regenerate-keys", request); + response.EnsureSuccessStatusCode(); + + var user = await _userRepository.GetByEmailAsync(_ownerEmail); + Assert.NotNull(user); + Assert.Equal(request.UserPublicKey, user.PublicKey); + Assert.Equal(request.UserKeyEncryptedUserPrivateKey, user.PrivateKey); + } + + private async Task CreateOrganizationUserAsync(OrganizationUserStatusType organizationUserStatus) + { + var (_, organizationUser) = await OrganizationTestHelpers.SignUpAsync(_factory, + PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10, + paymentMethod: PaymentMethodType.Card); + organizationUser.Status = organizationUserStatus; + await _organizationUserRepository.ReplaceAsync(organizationUser); + } + + private async Task CreateDesignatedEmergencyAccessAsync(EmergencyAccessStatusType emergencyAccessStatus) + { + var tempEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(tempEmail); + + var tempUser = await _userRepository.GetByEmailAsync(tempEmail); + var user = await _userRepository.GetByEmailAsync(_ownerEmail); + var emergencyAccess = new EmergencyAccess + { + GrantorId = tempUser!.Id, + GranteeId = user!.Id, + KeyEncrypted = _mockEncryptedString, + Status = emergencyAccessStatus, + Type = EmergencyAccessType.View, + WaitTimeDays = 10, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow + }; + await _emergencyAccessRepository.CreateAsync(emergencyAccess); + } +} diff --git a/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs new file mode 100644 index 0000000000..2615697ad3 --- /dev/null +++ b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -0,0 +1,96 @@ +#nullable enable +using System.Security.Claims; +using Bit.Api.KeyManagement.Controllers; +using Bit.Api.KeyManagement.Models.Requests; +using Bit.Core; +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.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Xunit; + +namespace Bit.Api.Test.KeyManagement.Controllers; + +[ControllerCustomize(typeof(AccountsKeyManagementController))] +[SutProviderCustomize] +[JsonDocumentCustomize] +public class AccountsKeyManagementControllerTests +{ + [Theory] + [BitAutoData] + public async Task RegenerateKeysAsync_FeatureFlagOff_Throws( + SutProvider sutProvider, + KeyRegenerationRequestModel data) + { + sutProvider.GetDependency().IsEnabled(Arg.Is(FeatureFlagKeys.PrivateKeyRegeneration)) + .Returns(false); + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).ReturnsNull(); + + await Assert.ThrowsAsync(() => sutProvider.Sut.RegenerateKeysAsync(data)); + + await sutProvider.GetDependency().ReceivedWithAnyArgs(0) + .GetManyByUserAsync(Arg.Any()); + await sutProvider.GetDependency().ReceivedWithAnyArgs(0) + .GetManyDetailsByGranteeIdAsync(Arg.Any()); + await sutProvider.GetDependency().ReceivedWithAnyArgs(0) + .RegenerateKeysAsync(Arg.Any(), + Arg.Any>(), + Arg.Any>()); + } + + [Theory] + [BitAutoData] + public async Task RegenerateKeysAsync_UserNull_Throws(SutProvider sutProvider, + KeyRegenerationRequestModel data) + { + sutProvider.GetDependency().IsEnabled(Arg.Is(FeatureFlagKeys.PrivateKeyRegeneration)) + .Returns(true); + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).ReturnsNull(); + + await Assert.ThrowsAsync(() => sutProvider.Sut.RegenerateKeysAsync(data)); + + await sutProvider.GetDependency().ReceivedWithAnyArgs(0) + .GetManyByUserAsync(Arg.Any()); + await sutProvider.GetDependency().ReceivedWithAnyArgs(0) + .GetManyDetailsByGranteeIdAsync(Arg.Any()); + await sutProvider.GetDependency().ReceivedWithAnyArgs(0) + .RegenerateKeysAsync(Arg.Any(), + Arg.Any>(), + Arg.Any>()); + } + + [Theory] + [BitAutoData] + public async Task RegenerateKeysAsync_Success(SutProvider sutProvider, + KeyRegenerationRequestModel data, User user, ICollection orgUsers, + ICollection accessDetails) + { + sutProvider.GetDependency().IsEnabled(Arg.Is(FeatureFlagKeys.PrivateKeyRegeneration)) + .Returns(true); + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).Returns(user); + sutProvider.GetDependency().GetManyByUserAsync(Arg.Is(user.Id)).Returns(orgUsers); + sutProvider.GetDependency().GetManyDetailsByGranteeIdAsync(Arg.Is(user.Id)) + .Returns(accessDetails); + + await sutProvider.Sut.RegenerateKeysAsync(data); + + await sutProvider.GetDependency().Received(1) + .GetManyByUserAsync(Arg.Is(user.Id)); + await sutProvider.GetDependency().Received(1) + .GetManyDetailsByGranteeIdAsync(Arg.Is(user.Id)); + await sutProvider.GetDependency().Received(1) + .RegenerateKeysAsync( + Arg.Is(u => + u.UserId == user.Id && u.PublicKey == data.UserPublicKey && + u.UserKeyEncryptedPrivateKey == data.UserKeyEncryptedUserPrivateKey), + Arg.Is(orgUsers), + Arg.Is(accessDetails)); + } +} diff --git a/test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs b/test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs new file mode 100644 index 0000000000..3388956156 --- /dev/null +++ b/test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs @@ -0,0 +1,197 @@ +#nullable enable +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Commands; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Xunit; + +namespace Bit.Core.Test.KeyManagement.Commands; + +[SutProviderCustomize] +public class RegenerateUserAsymmetricKeysCommandTests +{ + [Theory] + [BitAutoData] + public async Task RegenerateKeysAsync_NoCurrentContext_NotFoundException( + SutProvider sutProvider, + UserAsymmetricKeys userAsymmetricKeys) + { + sutProvider.GetDependency().UserId.ReturnsNullForAnyArgs(); + var usersOrganizationAccounts = new List(); + var designatedEmergencyAccess = new List(); + + await Assert.ThrowsAsync(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys, + usersOrganizationAccounts, designatedEmergencyAccess)); + } + + [Theory] + [BitAutoData] + public async Task RegenerateKeysAsync_UserHasNoSharedAccess_Success( + SutProvider sutProvider, + UserAsymmetricKeys userAsymmetricKeys) + { + sutProvider.GetDependency().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId); + var usersOrganizationAccounts = new List(); + var designatedEmergencyAccess = new List(); + + await sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys, + usersOrganizationAccounts, designatedEmergencyAccess); + + await sutProvider.GetDependency() + .Received(1) + .RegenerateUserAsymmetricKeysAsync(Arg.Is(userAsymmetricKeys)); + await sutProvider.GetDependency() + .Received(1) + .PushSyncSettingsAsync(Arg.Is(userAsymmetricKeys.UserId)); + } + + [Theory] + [BitAutoData(false, false, true)] + [BitAutoData(false, true, false)] + [BitAutoData(false, true, true)] + [BitAutoData(true, false, false)] + [BitAutoData(true, false, true)] + [BitAutoData(true, true, false)] + [BitAutoData(true, true, true)] + public async Task RegenerateKeysAsync_UserIdMisMatch_NotFoundException( + bool userAsymmetricKeysMismatch, + bool orgMismatch, + bool emergencyAccessMismatch, + SutProvider sutProvider, + UserAsymmetricKeys userAsymmetricKeys, + ICollection usersOrganizationAccounts, + ICollection designatedEmergencyAccess) + { + sutProvider.GetDependency().UserId + .ReturnsForAnyArgs(userAsymmetricKeysMismatch ? new Guid() : userAsymmetricKeys.UserId); + + if (!orgMismatch) + { + usersOrganizationAccounts = + SetupOrganizationUserAccounts(userAsymmetricKeys.UserId, usersOrganizationAccounts); + } + + if (!emergencyAccessMismatch) + { + designatedEmergencyAccess = SetupEmergencyAccess(userAsymmetricKeys.UserId, designatedEmergencyAccess); + } + + await Assert.ThrowsAsync(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys, + usersOrganizationAccounts, designatedEmergencyAccess)); + + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(0) + .RegenerateUserAsymmetricKeysAsync(Arg.Any()); + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(0) + .PushSyncSettingsAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(OrganizationUserStatusType.Confirmed)] + [BitAutoData(OrganizationUserStatusType.Revoked)] + public async Task RegenerateKeysAsync_UserInOrganizations_BadRequestException( + OrganizationUserStatusType organizationUserStatus, + SutProvider sutProvider, + UserAsymmetricKeys userAsymmetricKeys, + ICollection usersOrganizationAccounts) + { + sutProvider.GetDependency().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId); + usersOrganizationAccounts = CreateInOrganizationAccounts(userAsymmetricKeys.UserId, organizationUserStatus, + usersOrganizationAccounts); + var designatedEmergencyAccess = new List(); + + await Assert.ThrowsAsync(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys, + usersOrganizationAccounts, designatedEmergencyAccess)); + + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(0) + .RegenerateUserAsymmetricKeysAsync(Arg.Any()); + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(0) + .PushSyncSettingsAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + [BitAutoData(EmergencyAccessStatusType.RecoveryApproved)] + [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] + public async Task RegenerateKeysAsync_UserHasDesignatedEmergencyAccess_BadRequestException( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + UserAsymmetricKeys userAsymmetricKeys, + ICollection designatedEmergencyAccess) + { + sutProvider.GetDependency().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId); + designatedEmergencyAccess = + CreateDesignatedEmergencyAccess(userAsymmetricKeys.UserId, statusType, designatedEmergencyAccess); + var usersOrganizationAccounts = new List(); + + + await Assert.ThrowsAsync(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys, + usersOrganizationAccounts, designatedEmergencyAccess)); + + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(0) + .RegenerateUserAsymmetricKeysAsync(Arg.Any()); + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(0) + .PushSyncSettingsAsync(Arg.Any()); + } + + private static ICollection CreateInOrganizationAccounts(Guid userId, + OrganizationUserStatusType organizationUserStatus, ICollection organizationUserAccounts) + { + foreach (var organizationUserAccount in organizationUserAccounts) + { + organizationUserAccount.UserId = userId; + organizationUserAccount.Status = organizationUserStatus; + } + + return organizationUserAccounts; + } + + private static ICollection CreateDesignatedEmergencyAccess(Guid userId, + EmergencyAccessStatusType status, ICollection designatedEmergencyAccess) + { + foreach (var designated in designatedEmergencyAccess) + { + designated.GranteeId = userId; + designated.Status = status; + } + + return designatedEmergencyAccess; + } + + private static ICollection SetupOrganizationUserAccounts(Guid userId, + ICollection organizationUserAccounts) + { + foreach (var organizationUserAccount in organizationUserAccounts) + { + organizationUserAccount.UserId = userId; + } + + return organizationUserAccounts; + } + + private static ICollection SetupEmergencyAccess(Guid userId, + ICollection emergencyAccessDetails) + { + foreach (var emergencyAccessDetail in emergencyAccessDetails) + { + emergencyAccessDetail.GranteeId = userId; + } + + return emergencyAccessDetails; + } +} From b907935edaf0542cc2d8915b076d99e5b398cf62 Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Mon, 16 Dec 2024 16:18:33 -0500 Subject: [PATCH 84/94] Add Authenticator sync flags (#5159) * Add Authenticator sync flags * Fix whitespace --- src/Core/Constants.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 2c315b2578..86e49fa6cf 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -161,6 +161,8 @@ public static class FeatureFlagKeys public const string SelfHostLicenseRefactor = "pm-11516-self-host-license-refactor"; public const string PromoteProviderServiceUserTool = "pm-15128-promote-provider-service-user-tool"; public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration"; + public const string AuthenticatorSynciOS = "enable-authenticator-sync-ios"; + public const string AuthenticatorSyncAndroid = "enable-authenticator-sync-android"; public static List GetAllKeys() { From ecbfc056830e41822e4567721be0a8bc51fc0436 Mon Sep 17 00:00:00 2001 From: aj-bw <81774843+aj-bw@users.noreply.github.com> Date: Tue, 17 Dec 2024 08:32:37 -0500 Subject: [PATCH 85/94] QA-689/BEEEP-public-api-GET-subscription-details (#5041) * added GET operation to org subscription endpoint * adding back removed using statement * addressing unused import and lint warnings * whitespace lint fix * successful local format * add NotSelfHostOnly attribute * add endpoint summary and return details --- .../Controllers/OrganizationController.cs | 44 +++++++++++++++++++ ...anizationSubscriptionUpdateRequestModel.cs | 0 ...izationSubscriptionDetailsResponseModel.cs | 32 ++++++++++++++ 3 files changed, 76 insertions(+) rename src/Api/Billing/Public/Models/{ => Request}/OrganizationSubscriptionUpdateRequestModel.cs (100%) create mode 100644 src/Api/Billing/Public/Models/Response/OrganizationSubscriptionDetailsResponseModel.cs diff --git a/src/Api/Billing/Public/Controllers/OrganizationController.cs b/src/Api/Billing/Public/Controllers/OrganizationController.cs index c696f2af50..7fcd94acd3 100644 --- a/src/Api/Billing/Public/Controllers/OrganizationController.cs +++ b/src/Api/Billing/Public/Controllers/OrganizationController.cs @@ -1,4 +1,5 @@ using System.Net; +using Bit.Api.Billing.Public.Models; using Bit.Api.Models.Public.Response; using Bit.Core.Context; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; @@ -35,6 +36,49 @@ public class OrganizationController : Controller _logger = logger; } + /// + /// Retrieves the subscription details for the current organization. + /// + /// + /// Returns an object containing the subscription details if successful. + /// + [HttpGet("subscription")] + [SelfHosted(NotSelfHostedOnly = true)] + [ProducesResponseType(typeof(OrganizationSubscriptionDetailsResponseModel), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.NotFound)] + public async Task GetSubscriptionAsync() + { + try + { + var organizationId = _currentContext.OrganizationId.Value; + var organization = await _organizationRepository.GetByIdAsync(organizationId); + + var subscriptionDetails = new OrganizationSubscriptionDetailsResponseModel + { + PasswordManager = new PasswordManagerSubscriptionDetails + { + Seats = organization.Seats, + MaxAutoScaleSeats = organization.MaxAutoscaleSeats, + Storage = organization.MaxStorageGb + }, + SecretsManager = new SecretsManagerSubscriptionDetails + { + Seats = organization.SmSeats, + MaxAutoScaleSeats = organization.MaxAutoscaleSmSeats, + ServiceAccounts = organization.SmServiceAccounts, + MaxAutoScaleServiceAccounts = organization.MaxAutoscaleSmServiceAccounts + } + }; + + return Ok(subscriptionDetails); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled error while retrieving the subscription details"); + return StatusCode(500, new { Message = "An error occurred while retrieving the subscription details." }); + } + } + /// /// Update the organization's current subscription for Password Manager and/or Secrets Manager. /// diff --git a/src/Api/Billing/Public/Models/OrganizationSubscriptionUpdateRequestModel.cs b/src/Api/Billing/Public/Models/Request/OrganizationSubscriptionUpdateRequestModel.cs similarity index 100% rename from src/Api/Billing/Public/Models/OrganizationSubscriptionUpdateRequestModel.cs rename to src/Api/Billing/Public/Models/Request/OrganizationSubscriptionUpdateRequestModel.cs diff --git a/src/Api/Billing/Public/Models/Response/OrganizationSubscriptionDetailsResponseModel.cs b/src/Api/Billing/Public/Models/Response/OrganizationSubscriptionDetailsResponseModel.cs new file mode 100644 index 0000000000..09aa7decc1 --- /dev/null +++ b/src/Api/Billing/Public/Models/Response/OrganizationSubscriptionDetailsResponseModel.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.Billing.Public.Models; + +public class OrganizationSubscriptionDetailsResponseModel : IValidatableObject +{ + public PasswordManagerSubscriptionDetails PasswordManager { get; set; } + public SecretsManagerSubscriptionDetails SecretsManager { get; set; } + public IEnumerable Validate(ValidationContext validationContext) + { + if (PasswordManager == null && SecretsManager == null) + { + yield return new ValidationResult("At least one of PasswordManager or SecretsManager must be provided."); + } + + yield return ValidationResult.Success; + } +} +public class PasswordManagerSubscriptionDetails +{ + public int? Seats { get; set; } + public int? MaxAutoScaleSeats { get; set; } + public short? Storage { get; set; } +} + +public class SecretsManagerSubscriptionDetails +{ + public int? Seats { get; set; } + public int? MaxAutoScaleSeats { get; set; } + public int? ServiceAccounts { get; set; } + public int? MaxAutoScaleServiceAccounts { get; set; } +} From 16488091d24f275885cab4777b9764f5847298c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ch=C4=99ci=C5=84ski?= Date: Tue, 17 Dec 2024 16:45:02 +0100 Subject: [PATCH 86/94] Remove is_workflow_call input from build workflow (#5161) --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb644fe8b5..420b9b6375 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -580,7 +580,6 @@ jobs: ref: 'main', inputs: { server_branch: process.env.GITHUB_REF - is_workflow_call: true } }); From b75c63c2c6ac335747958c0e72e2d4143a3520c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:57:31 +0000 Subject: [PATCH 87/94] [PM-15957] Fix: Domain Claim fails to enable Single Organization Policy, sends no emails and Revokes all users (#5147) * Add JSON-based stored procedure for updating account revision dates and modify existing procedure to use it * Refactor SingleOrgPolicyValidator to revoke only non-compliant organization users and update related tests --- .../SingleOrgPolicyValidator.cs | 11 +++- ...OrganizationUser_SetStatusForUsersById.sql | 2 +- ...tRevisionDateByOrganizationUserIdsJson.sql | 33 ++++++++++ .../SingleOrgPolicyValidatorTests.cs | 38 +++++++++-- ...2-11-00_BumpAccountRevisionDateJsonIds.sql | 64 +++++++++++++++++++ 5 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByOrganizationUserIdsJson.sql create mode 100644 util/Migrator/DbScripts/2024-12-11-00_BumpAccountRevisionDateJsonIds.sql diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs index 050949ee7f..a37deef3eb 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs @@ -97,15 +97,22 @@ public class SingleOrgPolicyValidator : IPolicyValidator return; } + var allRevocableUserOrgs = await _organizationUserRepository.GetManyByManyUsersAsync( + currentActiveRevocableOrganizationUsers.Select(ou => ou.UserId!.Value)); + var usersToRevoke = currentActiveRevocableOrganizationUsers.Where(ou => + allRevocableUserOrgs.Any(uo => uo.UserId == ou.UserId && + uo.OrganizationId != organizationId && + uo.Status != OrganizationUserStatusType.Invited)).ToList(); + var commandResult = await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync( - new RevokeOrganizationUsersRequest(organizationId, currentActiveRevocableOrganizationUsers, performedBy)); + new RevokeOrganizationUsersRequest(organizationId, usersToRevoke, performedBy)); if (commandResult.HasErrors) { throw new BadRequestException(string.Join(", ", commandResult.ErrorMessages)); } - await Task.WhenAll(currentActiveRevocableOrganizationUsers.Select(x => + await Task.WhenAll(usersToRevoke.Select(x => _mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email))); } diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_SetStatusForUsersById.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_SetStatusForUsersById.sql index 95ed5a3155..18b876775e 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_SetStatusForUsersById.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_SetStatusForUsersById.sql @@ -24,6 +24,6 @@ BEGIN SET [Status] = @Status WHERE [Id] IN (SELECT Id from @ParsedIds) - EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @OrganizationUserIds + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIdsJson] @OrganizationUserIds END diff --git a/src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByOrganizationUserIdsJson.sql b/src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByOrganizationUserIdsJson.sql new file mode 100644 index 0000000000..6e4119d864 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByOrganizationUserIdsJson.sql @@ -0,0 +1,33 @@ +CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByOrganizationUserIdsJson] + @OrganizationUserIds NVARCHAR(MAX) +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #UserIds + ( + UserId UNIQUEIDENTIFIER NOT NULL + ); + + INSERT INTO #UserIds (UserId) + SELECT + OU.UserId + FROM + [dbo].[OrganizationUser] OU + INNER JOIN + (SELECT [value] as Id FROM OPENJSON(@OrganizationUserIds)) AS OUIds + ON OUIds.Id = OU.Id + WHERE + OU.[Status] = 2 -- Confirmed + + UPDATE + U + SET + U.[AccountRevisionDate] = GETUTCDATE() + FROM + [dbo].[User] U + INNER JOIN + #UserIds ON U.[Id] = #UserIds.[UserId] + + DROP TABLE #UserIds +END diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs index 0731920757..d2809102aa 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs @@ -75,6 +75,7 @@ public class SingleOrgPolicyValidatorTests var compliantUser1 = new OrganizationUserUserDetails { + Id = Guid.NewGuid(), OrganizationId = organization.Id, Type = OrganizationUserType.User, Status = OrganizationUserStatusType.Confirmed, @@ -84,6 +85,7 @@ public class SingleOrgPolicyValidatorTests var compliantUser2 = new OrganizationUserUserDetails { + Id = Guid.NewGuid(), OrganizationId = organization.Id, Type = OrganizationUserType.User, Status = OrganizationUserStatusType.Confirmed, @@ -93,6 +95,7 @@ public class SingleOrgPolicyValidatorTests var nonCompliantUser = new OrganizationUserUserDetails { + Id = Guid.NewGuid(), OrganizationId = organization.Id, Type = OrganizationUserType.User, Status = OrganizationUserStatusType.Confirmed, @@ -106,6 +109,7 @@ public class SingleOrgPolicyValidatorTests var otherOrganizationUser = new OrganizationUser { + Id = Guid.NewGuid(), OrganizationId = new Guid(), UserId = nonCompliantUserId, Status = OrganizationUserStatusType.Confirmed @@ -129,11 +133,20 @@ public class SingleOrgPolicyValidatorTests await sutProvider.GetDependency() .Received(1) - .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()); + .RevokeNonCompliantOrganizationUsersAsync( + Arg.Is(r => + r.OrganizationId == organization.Id && + r.OrganizationUsers.Count() == 1 && + r.OrganizationUsers.First().Id == nonCompliantUser.Id)); + await sutProvider.GetDependency() + .DidNotReceive() + .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser1.Email); + await sutProvider.GetDependency() + .DidNotReceive() + .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser2.Email); await sutProvider.GetDependency() .Received(1) - .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), - "user3@example.com"); + .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email); } [Theory, BitAutoData] @@ -148,6 +161,7 @@ public class SingleOrgPolicyValidatorTests var compliantUser1 = new OrganizationUserUserDetails { + Id = Guid.NewGuid(), OrganizationId = organization.Id, Type = OrganizationUserType.User, Status = OrganizationUserStatusType.Confirmed, @@ -157,6 +171,7 @@ public class SingleOrgPolicyValidatorTests var compliantUser2 = new OrganizationUserUserDetails { + Id = Guid.NewGuid(), OrganizationId = organization.Id, Type = OrganizationUserType.User, Status = OrganizationUserStatusType.Confirmed, @@ -166,6 +181,7 @@ public class SingleOrgPolicyValidatorTests var nonCompliantUser = new OrganizationUserUserDetails { + Id = Guid.NewGuid(), OrganizationId = organization.Id, Type = OrganizationUserType.User, Status = OrganizationUserStatusType.Confirmed, @@ -179,6 +195,7 @@ public class SingleOrgPolicyValidatorTests var otherOrganizationUser = new OrganizationUser { + Id = Guid.NewGuid(), OrganizationId = new Guid(), UserId = nonCompliantUserId, Status = OrganizationUserStatusType.Confirmed @@ -200,13 +217,24 @@ public class SingleOrgPolicyValidatorTests await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy); + await sutProvider.GetDependency() + .DidNotReceive() + .RemoveUserAsync(policyUpdate.OrganizationId, compliantUser1.Id, savingUserId); + await sutProvider.GetDependency() + .DidNotReceive() + .RemoveUserAsync(policyUpdate.OrganizationId, compliantUser2.Id, savingUserId); await sutProvider.GetDependency() .Received(1) .RemoveUserAsync(policyUpdate.OrganizationId, nonCompliantUser.Id, savingUserId); + await sutProvider.GetDependency() + .DidNotReceive() + .SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser1.Email); + await sutProvider.GetDependency() + .DidNotReceive() + .SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser2.Email); await sutProvider.GetDependency() .Received(1) - .SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), - "user3@example.com"); + .SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email); } [Theory, BitAutoData] diff --git a/util/Migrator/DbScripts/2024-12-11-00_BumpAccountRevisionDateJsonIds.sql b/util/Migrator/DbScripts/2024-12-11-00_BumpAccountRevisionDateJsonIds.sql new file mode 100644 index 0000000000..11d1d75a31 --- /dev/null +++ b/util/Migrator/DbScripts/2024-12-11-00_BumpAccountRevisionDateJsonIds.sql @@ -0,0 +1,64 @@ +CREATE OR ALTER PROCEDURE [dbo].[User_BumpAccountRevisionDateByOrganizationUserIdsJson] + @OrganizationUserIds NVARCHAR(MAX) +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #UserIds + ( + UserId UNIQUEIDENTIFIER NOT NULL + ); + + INSERT INTO #UserIds (UserId) + SELECT + OU.UserId + FROM + [dbo].[OrganizationUser] OU + INNER JOIN + (SELECT [value] as Id FROM OPENJSON(@OrganizationUserIds)) AS OUIds + ON OUIds.Id = OU.Id + WHERE + OU.[Status] = 2 -- Confirmed + + UPDATE + U + SET + U.[AccountRevisionDate] = GETUTCDATE() + FROM + [dbo].[User] U + INNER JOIN + #UserIds ON U.[Id] = #UserIds.[UserId] + + DROP TABLE #UserIds +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_SetStatusForUsersById] + @OrganizationUserIds AS NVARCHAR(MAX), + @Status SMALLINT +AS +BEGIN + SET NOCOUNT ON + + -- Declare a table variable to hold the parsed JSON data + DECLARE @ParsedIds TABLE (Id UNIQUEIDENTIFIER); + + -- Parse the JSON input into the table variable + INSERT INTO @ParsedIds (Id) + SELECT value + FROM OPENJSON(@OrganizationUserIds); + + -- Check if the input table is empty + IF (SELECT COUNT(1) FROM @ParsedIds) < 1 + BEGIN + RETURN(-1); + END + + UPDATE + [dbo].[OrganizationUser] + SET [Status] = @Status + WHERE [Id] IN (SELECT Id from @ParsedIds) + + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIdsJson] @OrganizationUserIds +END +GO From 2e8f2df9428edf19a272141a8d36dc7ddad20ed4 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Tue, 17 Dec 2024 08:59:39 -0800 Subject: [PATCH 88/94] feat(NewDeviceVerification) : (#5153) feat(NewDeviceVerification) : Added constat for the cache key in Bit.Core because the cache key format needs to be shared between the Identity Server and the MVC Admin project. Updated DeviceValidator class to handle checking cache for user information to allow pass through. Updated and Added tests to handle new flow. --- src/Core/Constants.cs | 2 +- .../RequestValidators/DeviceValidator.cs | 18 ++++++++- .../IdentityServer/DeviceValidatorTests.cs | 38 ++++++++++++++++++- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 86e49fa6cf..9b51b12d62 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -57,7 +57,7 @@ public static class AuthConstants public static readonly RangeConstant ARGON2_ITERATIONS = new(2, 10, 3); public static readonly RangeConstant ARGON2_MEMORY = new(15, 1024, 64); public static readonly RangeConstant ARGON2_PARALLELISM = new(1, 16, 4); - + public static readonly string NewDeviceVerificationExceptionCacheKeyFormat = "NewDeviceVerificationException_{0}"; } public class RangeConstant diff --git a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs index 2a048bcb2a..d59417bfa7 100644 --- a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs @@ -10,6 +10,7 @@ using Bit.Core.Services; using Bit.Core.Settings; using Bit.Identity.IdentityServer.Enums; using Duende.IdentityServer.Validation; +using Microsoft.Extensions.Caching.Distributed; namespace Bit.Identity.IdentityServer.RequestValidators; @@ -20,6 +21,8 @@ public class DeviceValidator( IMailService mailService, ICurrentContext currentContext, IUserService userService, + IDistributedCache distributedCache, + ILogger logger, IFeatureService featureService) : IDeviceValidator { private readonly IDeviceService _deviceService = deviceService; @@ -28,6 +31,8 @@ public class DeviceValidator( private readonly IMailService _mailService = mailService; private readonly ICurrentContext _currentContext = currentContext; private readonly IUserService _userService = userService; + private readonly IDistributedCache distributedCache = distributedCache; + private readonly ILogger _logger = logger; private readonly IFeatureService _featureService = featureService; public async Task ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context) @@ -67,7 +72,6 @@ public class DeviceValidator( !context.SsoRequired && _globalSettings.EnableNewDeviceVerification) { - // We only want to return early if the device is invalid or there is an error var validationResult = await HandleNewDeviceVerificationAsync(context.User, request); if (validationResult != DeviceValidationResultType.Success) { @@ -121,6 +125,18 @@ public class DeviceValidator( return DeviceValidationResultType.InvalidUser; } + // CS exception flow + // Check cache for user information + var cacheKey = string.Format(AuthConstants.NewDeviceVerificationExceptionCacheKeyFormat, user.Id.ToString()); + var cacheValue = await distributedCache.GetAsync(cacheKey); + if (cacheValue != null) + { + // if found in cache return success result and remove from cache + await distributedCache.RemoveAsync(cacheKey); + _logger.LogInformation("New device verification exception for user {UserId} found in cache", user.Id); + return DeviceValidationResultType.Success; + } + // parse request for NewDeviceOtp to validate var newDeviceOtp = request.Raw["NewDeviceOtp"]?.ToString(); // we only check null here since an empty OTP will be considered an incorrect OTP diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index 304715b68c..105267ea30 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -10,6 +10,8 @@ using Bit.Identity.IdentityServer; using Bit.Identity.IdentityServer.RequestValidators; using Bit.Test.Common.AutoFixture.Attributes; using Duende.IdentityServer.Validation; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; using AuthFixtures = Bit.Identity.Test.AutoFixture; @@ -24,6 +26,8 @@ public class DeviceValidatorTests private readonly IMailService _mailService; private readonly ICurrentContext _currentContext; private readonly IUserService _userService; + private readonly IDistributedCache _distributedCache; + private readonly Logger _logger; private readonly IFeatureService _featureService; private readonly DeviceValidator _sut; @@ -35,6 +39,8 @@ public class DeviceValidatorTests _mailService = Substitute.For(); _currentContext = Substitute.For(); _userService = Substitute.For(); + _distributedCache = Substitute.For(); + _logger = new Logger(Substitute.For()); _featureService = Substitute.For(); _sut = new DeviceValidator( _deviceService, @@ -43,6 +49,8 @@ public class DeviceValidatorTests _mailService, _currentContext, _userService, + _distributedCache, + _logger, _featureService); } @@ -51,7 +59,7 @@ public class DeviceValidatorTests Device device) { // Arrange - // AutoData arrages + // AutoData arranges // Act var result = await _sut.GetKnownDeviceAsync(null, device); @@ -421,6 +429,30 @@ public class DeviceValidatorTests Assert.Equal(expectedErrorMessage, actualResponse.Message); } + [Theory, BitAutoData] + public async void HandleNewDeviceVerificationAsync_UserHasCacheValue_ReturnsSuccess( + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + ArrangeForHandleNewDeviceVerificationTest(context, request); + _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); + _globalSettings.EnableNewDeviceVerification = true; + _distributedCache.GetAsync(Arg.Any()).Returns([1]); + + // Act + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + await _userService.Received(0).SendOTPAsync(context.User); + await _deviceService.Received(1).SaveAsync(Arg.Any()); + + Assert.True(result); + Assert.False(context.CustomResponse.ContainsKey("ErrorModel")); + Assert.Equal(context.User.Id, context.Device.UserId); + Assert.NotNull(context.Device); + } + [Theory, BitAutoData] public async void HandleNewDeviceVerificationAsync_NewDeviceOtpValid_ReturnsSuccess( CustomValidatorRequestContext context, @@ -430,6 +462,7 @@ public class DeviceValidatorTests ArrangeForHandleNewDeviceVerificationTest(context, request); _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; + _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); var newDeviceOtp = "123456"; request.Raw.Add("NewDeviceOtp", newDeviceOtp); @@ -461,6 +494,7 @@ public class DeviceValidatorTests ArrangeForHandleNewDeviceVerificationTest(context, request); _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; + _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); request.Raw.Add("NewDeviceOtp", newDeviceOtp); @@ -489,6 +523,7 @@ public class DeviceValidatorTests ArrangeForHandleNewDeviceVerificationTest(context, request); _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; + _distributedCache.GetAsync(Arg.Any()).Returns([1]); _deviceRepository.GetManyByUserIdAsync(context.User.Id).Returns([]); // Act @@ -515,6 +550,7 @@ public class DeviceValidatorTests _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _deviceRepository.GetManyByUserIdAsync(context.User.Id).Returns([new Device()]); + _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); // Act var result = await _sut.ValidateRequestDeviceAsync(request, context); From eb9a061e6f84bbdb124b37db6895825a2bd4797c Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Tue, 17 Dec 2024 12:44:08 -0500 Subject: [PATCH 89/94] [pm-15123] Add delete permissions for CS and Billing. (#5145) --- src/Admin/Utilities/RolePermissionMapping.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Admin/Utilities/RolePermissionMapping.cs b/src/Admin/Utilities/RolePermissionMapping.cs index 81da3fcf38..9cee571aba 100644 --- a/src/Admin/Utilities/RolePermissionMapping.cs +++ b/src/Admin/Utilities/RolePermissionMapping.cs @@ -114,6 +114,7 @@ public static class RolePermissionMapping Permission.User_Billing_LaunchGateway, Permission.Org_List_View, Permission.Org_OrgInformation_View, + Permission.Org_Delete, Permission.Org_GeneralDetails_View, Permission.Org_BusinessInformation_View, Permission.Org_BillingInformation_View, @@ -156,6 +157,7 @@ public static class RolePermissionMapping Permission.Org_Billing_View, Permission.Org_Billing_Edit, Permission.Org_Billing_LaunchGateway, + Permission.Org_Delete, Permission.Provider_Edit, Permission.Provider_View, Permission.Provider_List_View, From de2dc243fc81fe4966941c6ca7fc712fc0592904 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:18:37 +0100 Subject: [PATCH 90/94] [deps] Tools: Update MailKit to 4.9.0 (#5133) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 9049f94dcf..43068a4ac0 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -34,7 +34,7 @@ - + From 21fcfcd5e855b72928f853f771c4980a5f9ccbc9 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:59:50 +0100 Subject: [PATCH 91/94] [PM-10563] Notification Center API (#4852) * PM-10563: Notification Center API * PM-10563: continuation token hack * PM-10563: Resolving merge conflicts * PM-10563: Unit Tests * PM-10563: Paging simplification by page number and size in database * PM-10563: Request validation * PM-10563: Read, Deleted status filters change * PM-10563: Plural name for tests * PM-10563: Request validation to always for int type * PM-10563: Continuation Token returns null on response when no more records available * PM-10563: Integration tests for GET * PM-10563: Mark notification read, deleted commands date typos fix * PM-10563: Integration tests for PATCH read, deleted * PM-10563: Request, Response models tests * PM-10563: EditorConfig compliance * PM-10563: Extracting to const * PM-10563: Update db migration script date * PM-10563: Update migration script date --- .../Controllers/NotificationsController.cs | 71 +++ .../Request/NotificationFilterRequestModel.cs | 41 ++ .../Response/NotificationResponseModel.cs | 46 ++ ...cationCenterServiceCollectionExtensions.cs | 29 + ...etNotificationStatusDetailsForUserQuery.cs | 7 +- ...etNotificationStatusDetailsForUserQuery.cs | 4 +- .../Repositories/INotificationRepository.cs | 10 +- .../Repositories/NotificationRepository.cs | 28 +- .../Repositories/NotificationRepository.cs | 28 +- .../Utilities/ServiceCollectionExtensions.cs | 2 + .../Notification_ReadByUserIdAndStatus.sql | 15 +- .../NotificationsControllerTests.cs | 582 ++++++++++++++++++ .../NotificationsControllerTests.cs | 202 ++++++ .../NotificationFilterRequestModelTests.cs | 93 +++ .../NotificationResponseModelTests.cs | 43 ++ .../NotificationStatusDetailsFixtures.cs | 34 +- ...tificationStatusDetailsForUserQueryTest.cs | 37 +- ...4-12-18_00_AddPagingToNotificationRead.sql | 39 ++ 18 files changed, 1272 insertions(+), 39 deletions(-) create mode 100644 src/Api/NotificationCenter/Controllers/NotificationsController.cs create mode 100644 src/Api/NotificationCenter/Models/Request/NotificationFilterRequestModel.cs create mode 100644 src/Api/NotificationCenter/Models/Response/NotificationResponseModel.cs create mode 100644 src/Core/NotificationCenter/NotificationCenterServiceCollectionExtensions.cs create mode 100644 test/Api.IntegrationTest/NotificationCenter/Controllers/NotificationsControllerTests.cs create mode 100644 test/Api.Test/NotificationCenter/Controllers/NotificationsControllerTests.cs create mode 100644 test/Api.Test/NotificationCenter/Models/Request/NotificationFilterRequestModelTests.cs create mode 100644 test/Api.Test/NotificationCenter/Models/Response/NotificationResponseModelTests.cs create mode 100644 util/Migrator/DbScripts/2024-12-18_00_AddPagingToNotificationRead.sql diff --git a/src/Api/NotificationCenter/Controllers/NotificationsController.cs b/src/Api/NotificationCenter/Controllers/NotificationsController.cs new file mode 100644 index 0000000000..9dc1505cb8 --- /dev/null +++ b/src/Api/NotificationCenter/Controllers/NotificationsController.cs @@ -0,0 +1,71 @@ +#nullable enable +using Bit.Api.Models.Response; +using Bit.Api.NotificationCenter.Models.Request; +using Bit.Api.NotificationCenter.Models.Response; +using Bit.Core.Models.Data; +using Bit.Core.NotificationCenter.Commands.Interfaces; +using Bit.Core.NotificationCenter.Models.Filter; +using Bit.Core.NotificationCenter.Queries.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.NotificationCenter.Controllers; + +[Route("notifications")] +[Authorize("Application")] +public class NotificationsController : Controller +{ + private readonly IGetNotificationStatusDetailsForUserQuery _getNotificationStatusDetailsForUserQuery; + private readonly IMarkNotificationDeletedCommand _markNotificationDeletedCommand; + private readonly IMarkNotificationReadCommand _markNotificationReadCommand; + + public NotificationsController( + IGetNotificationStatusDetailsForUserQuery getNotificationStatusDetailsForUserQuery, + IMarkNotificationDeletedCommand markNotificationDeletedCommand, + IMarkNotificationReadCommand markNotificationReadCommand) + { + _getNotificationStatusDetailsForUserQuery = getNotificationStatusDetailsForUserQuery; + _markNotificationDeletedCommand = markNotificationDeletedCommand; + _markNotificationReadCommand = markNotificationReadCommand; + } + + [HttpGet("")] + public async Task> ListAsync( + [FromQuery] NotificationFilterRequestModel filter) + { + var pageOptions = new PageOptions + { + ContinuationToken = filter.ContinuationToken, + PageSize = filter.PageSize + }; + + var notificationStatusFilter = new NotificationStatusFilter + { + Read = filter.ReadStatusFilter, + Deleted = filter.DeletedStatusFilter + }; + + var notificationStatusDetailsPagedResult = + await _getNotificationStatusDetailsForUserQuery.GetByUserIdStatusFilterAsync(notificationStatusFilter, + pageOptions); + + var responses = notificationStatusDetailsPagedResult.Data + .Select(n => new NotificationResponseModel(n)) + .ToList(); + + return new ListResponseModel(responses, + notificationStatusDetailsPagedResult.ContinuationToken); + } + + [HttpPatch("{id}/delete")] + public async Task MarkAsDeletedAsync([FromRoute] Guid id) + { + await _markNotificationDeletedCommand.MarkDeletedAsync(id); + } + + [HttpPatch("{id}/read")] + public async Task MarkAsReadAsync([FromRoute] Guid id) + { + await _markNotificationReadCommand.MarkReadAsync(id); + } +} diff --git a/src/Api/NotificationCenter/Models/Request/NotificationFilterRequestModel.cs b/src/Api/NotificationCenter/Models/Request/NotificationFilterRequestModel.cs new file mode 100644 index 0000000000..9c6252b6db --- /dev/null +++ b/src/Api/NotificationCenter/Models/Request/NotificationFilterRequestModel.cs @@ -0,0 +1,41 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.NotificationCenter.Models.Request; + +public class NotificationFilterRequestModel : IValidatableObject +{ + /// + /// Filters notifications by read status. When not set, includes notifications without a status. + /// + public bool? ReadStatusFilter { get; set; } + + /// + /// Filters notifications by deleted status. When not set, includes notifications without a status. + /// + public bool? DeletedStatusFilter { get; set; } + + /// + /// A cursor for use in pagination. + /// + [StringLength(9)] + public string? ContinuationToken { get; set; } + + /// + /// The number of items to return in a single page. + /// Default 10. Minimum 10, maximum 1000. + /// + [Range(10, 1000)] + public int PageSize { get; set; } = 10; + + public IEnumerable Validate(ValidationContext validationContext) + { + if (!string.IsNullOrWhiteSpace(ContinuationToken) && + (!int.TryParse(ContinuationToken, out var pageNumber) || pageNumber <= 0)) + { + yield return new ValidationResult( + "Continuation token must be a positive, non zero integer.", + [nameof(ContinuationToken)]); + } + } +} diff --git a/src/Api/NotificationCenter/Models/Response/NotificationResponseModel.cs b/src/Api/NotificationCenter/Models/Response/NotificationResponseModel.cs new file mode 100644 index 0000000000..1ebed87de2 --- /dev/null +++ b/src/Api/NotificationCenter/Models/Response/NotificationResponseModel.cs @@ -0,0 +1,46 @@ +#nullable enable +using Bit.Core.Models.Api; +using Bit.Core.NotificationCenter.Enums; +using Bit.Core.NotificationCenter.Models.Data; + +namespace Bit.Api.NotificationCenter.Models.Response; + +public class NotificationResponseModel : ResponseModel +{ + private const string _objectName = "notification"; + + public NotificationResponseModel(NotificationStatusDetails notificationStatusDetails, string obj = _objectName) + : base(obj) + { + if (notificationStatusDetails == null) + { + throw new ArgumentNullException(nameof(notificationStatusDetails)); + } + + Id = notificationStatusDetails.Id; + Priority = notificationStatusDetails.Priority; + Title = notificationStatusDetails.Title; + Body = notificationStatusDetails.Body; + Date = notificationStatusDetails.RevisionDate; + ReadDate = notificationStatusDetails.ReadDate; + DeletedDate = notificationStatusDetails.DeletedDate; + } + + public NotificationResponseModel() : base(_objectName) + { + } + + public Guid Id { get; set; } + + public Priority Priority { get; set; } + + public string? Title { get; set; } + + public string? Body { get; set; } + + public DateTime Date { get; set; } + + public DateTime? ReadDate { get; set; } + + public DateTime? DeletedDate { get; set; } +} diff --git a/src/Core/NotificationCenter/NotificationCenterServiceCollectionExtensions.cs b/src/Core/NotificationCenter/NotificationCenterServiceCollectionExtensions.cs new file mode 100644 index 0000000000..fe41ebc5c3 --- /dev/null +++ b/src/Core/NotificationCenter/NotificationCenterServiceCollectionExtensions.cs @@ -0,0 +1,29 @@ +#nullable enable +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands; +using Bit.Core.NotificationCenter.Commands.Interfaces; +using Bit.Core.NotificationCenter.Queries; +using Bit.Core.NotificationCenter.Queries.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.NotificationCenter; + +public static class NotificationCenterServiceCollectionExtensions +{ + public static void AddNotificationCenterServices(this IServiceCollection services) + { + // Authorization Handlers + services.AddScoped(); + services.AddScoped(); + // Commands + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + // Queries + services.AddScoped(); + services.AddScoped(); + } +} diff --git a/src/Core/NotificationCenter/Queries/GetNotificationStatusDetailsForUserQuery.cs b/src/Core/NotificationCenter/Queries/GetNotificationStatusDetailsForUserQuery.cs index 0a783a59ba..235c2c6ed0 100644 --- a/src/Core/NotificationCenter/Queries/GetNotificationStatusDetailsForUserQuery.cs +++ b/src/Core/NotificationCenter/Queries/GetNotificationStatusDetailsForUserQuery.cs @@ -1,6 +1,7 @@ #nullable enable using Bit.Core.Context; using Bit.Core.Exceptions; +using Bit.Core.Models.Data; using Bit.Core.NotificationCenter.Models.Data; using Bit.Core.NotificationCenter.Models.Filter; using Bit.Core.NotificationCenter.Queries.Interfaces; @@ -21,8 +22,8 @@ public class GetNotificationStatusDetailsForUserQuery : IGetNotificationStatusDe _notificationRepository = notificationRepository; } - public async Task> GetByUserIdStatusFilterAsync( - NotificationStatusFilter statusFilter) + public async Task> GetByUserIdStatusFilterAsync( + NotificationStatusFilter statusFilter, PageOptions pageOptions) { if (!_currentContext.UserId.HasValue) { @@ -33,6 +34,6 @@ public class GetNotificationStatusDetailsForUserQuery : IGetNotificationStatusDe // Note: only returns the user's notifications - no authorization check needed return await _notificationRepository.GetByUserIdAndStatusAsync(_currentContext.UserId.Value, clientType, - statusFilter); + statusFilter, pageOptions); } } diff --git a/src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationStatusDetailsForUserQuery.cs b/src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationStatusDetailsForUserQuery.cs index 456a0e9400..fd6c0b5e63 100644 --- a/src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationStatusDetailsForUserQuery.cs +++ b/src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationStatusDetailsForUserQuery.cs @@ -1,4 +1,5 @@ #nullable enable +using Bit.Core.Models.Data; using Bit.Core.NotificationCenter.Models.Data; using Bit.Core.NotificationCenter.Models.Filter; @@ -6,5 +7,6 @@ namespace Bit.Core.NotificationCenter.Queries.Interfaces; public interface IGetNotificationStatusDetailsForUserQuery { - Task> GetByUserIdStatusFilterAsync(NotificationStatusFilter statusFilter); + Task> GetByUserIdStatusFilterAsync(NotificationStatusFilter statusFilter, + PageOptions pageOptions); } diff --git a/src/Core/NotificationCenter/Repositories/INotificationRepository.cs b/src/Core/NotificationCenter/Repositories/INotificationRepository.cs index 2c3faed914..21604ed169 100644 --- a/src/Core/NotificationCenter/Repositories/INotificationRepository.cs +++ b/src/Core/NotificationCenter/Repositories/INotificationRepository.cs @@ -1,5 +1,6 @@ #nullable enable using Bit.Core.Enums; +using Bit.Core.Models.Data; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Models.Data; using Bit.Core.NotificationCenter.Models.Filter; @@ -22,10 +23,13 @@ public interface INotificationRepository : IRepository /// If both and /// are not set, includes notifications without a status. /// + /// + /// Pagination options. + /// /// - /// Ordered by priority (highest to lowest) and creation date (descending). + /// Paged results ordered by priority (descending, highest to lowest) and creation date (descending). /// Includes all fields from and /// - Task> GetByUserIdAndStatusAsync(Guid userId, ClientType clientType, - NotificationStatusFilter? statusFilter); + Task> GetByUserIdAndStatusAsync(Guid userId, ClientType clientType, + NotificationStatusFilter? statusFilter, PageOptions pageOptions); } diff --git a/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs b/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs index f70c50f49f..b6843d9801 100644 --- a/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs +++ b/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs @@ -1,6 +1,7 @@ #nullable enable using System.Data; using Bit.Core.Enums; +using Bit.Core.Models.Data; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Models.Data; using Bit.Core.NotificationCenter.Models.Filter; @@ -24,16 +25,35 @@ public class NotificationRepository : Repository, INotificat { } - public async Task> GetByUserIdAndStatusAsync(Guid userId, - ClientType clientType, NotificationStatusFilter? statusFilter) + public async Task> GetByUserIdAndStatusAsync(Guid userId, + ClientType clientType, NotificationStatusFilter? statusFilter, PageOptions pageOptions) { await using var connection = new SqlConnection(ConnectionString); + if (!int.TryParse(pageOptions.ContinuationToken, out var pageNumber)) + { + pageNumber = 1; + } + var results = await connection.QueryAsync( "[dbo].[Notification_ReadByUserIdAndStatus]", - new { UserId = userId, ClientType = clientType, statusFilter?.Read, statusFilter?.Deleted }, + new + { + UserId = userId, + ClientType = clientType, + statusFilter?.Read, + statusFilter?.Deleted, + PageNumber = pageNumber, + pageOptions.PageSize + }, commandType: CommandType.StoredProcedure); - return results.ToList(); + var data = results.ToList(); + + return new PagedResult + { + Data = data, + ContinuationToken = data.Count < pageOptions.PageSize ? null : (pageNumber + 1).ToString() + }; } } diff --git a/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs b/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs index a413e78748..5d1071f26c 100644 --- a/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs +++ b/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs @@ -1,6 +1,7 @@ #nullable enable using AutoMapper; using Bit.Core.Enums; +using Bit.Core.Models.Data; using Bit.Core.NotificationCenter.Models.Data; using Bit.Core.NotificationCenter.Models.Filter; using Bit.Core.NotificationCenter.Repositories; @@ -36,28 +37,41 @@ public class NotificationRepository : Repository>(notifications); } - public async Task> GetByUserIdAndStatusAsync(Guid userId, - ClientType clientType, NotificationStatusFilter? statusFilter) + public async Task> GetByUserIdAndStatusAsync(Guid userId, + ClientType clientType, NotificationStatusFilter? statusFilter, PageOptions pageOptions) { await using var scope = ServiceScopeFactory.CreateAsyncScope(); var dbContext = GetDatabaseContext(scope); + if (!int.TryParse(pageOptions.ContinuationToken, out var pageNumber)) + { + pageNumber = 1; + } + var notificationStatusDetailsViewQuery = new NotificationStatusDetailsViewQuery(userId, clientType); var query = notificationStatusDetailsViewQuery.Run(dbContext); if (statusFilter != null && (statusFilter.Read != null || statusFilter.Deleted != null)) { query = from n in query - where statusFilter.Read == null || - (statusFilter.Read == true ? n.ReadDate != null : n.ReadDate == null) || - statusFilter.Deleted == null || - (statusFilter.Deleted == true ? n.DeletedDate != null : n.DeletedDate == null) + where (statusFilter.Read == null || + (statusFilter.Read == true ? n.ReadDate != null : n.ReadDate == null)) && + (statusFilter.Deleted == null || + (statusFilter.Deleted == true ? n.DeletedDate != null : n.DeletedDate == null)) select n; } - return await query + var results = await query .OrderByDescending(n => n.Priority) .ThenByDescending(n => n.CreationDate) + .Skip(pageOptions.PageSize * (pageNumber - 1)) + .Take(pageOptions.PageSize) .ToListAsync(); + + return new PagedResult + { + Data = results, + ContinuationToken = results.Count < pageOptions.PageSize ? null : (pageNumber + 1).ToString() + }; } } diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index c757f163e9..85bd0301c3 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -27,6 +27,7 @@ using Bit.Core.HostedServices; using Bit.Core.Identity; using Bit.Core.IdentityServer; using Bit.Core.KeyManagement; +using Bit.Core.NotificationCenter; using Bit.Core.NotificationHub; using Bit.Core.OrganizationFeatures; using Bit.Core.Repositories; @@ -122,6 +123,7 @@ public static class ServiceCollectionExtensions services.AddVaultServices(); services.AddReportingServices(); services.AddKeyManagementServices(); + services.AddNotificationCenterServices(); } public static void AddTokenizers(this IServiceCollection services) diff --git a/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadByUserIdAndStatus.sql b/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadByUserIdAndStatus.sql index b98f85f73c..72efda2012 100644 --- a/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadByUserIdAndStatus.sql +++ b/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadByUserIdAndStatus.sql @@ -2,7 +2,9 @@ CREATE PROCEDURE [dbo].[Notification_ReadByUserIdAndStatus] @UserId UNIQUEIDENTIFIER, @ClientType TINYINT, @Read BIT, - @Deleted BIT + @Deleted BIT, + @PageNumber INT = 1, + @PageSize INT = 10 AS BEGIN SET NOCOUNT ON @@ -21,13 +23,14 @@ BEGIN AND ou.[OrganizationId] IS NOT NULL)) AND ((@Read IS NULL AND @Deleted IS NULL) OR (n.[NotificationStatusUserId] IS NOT NULL - AND ((@Read IS NULL + AND (@Read IS NULL OR IIF((@Read = 1 AND n.[ReadDate] IS NOT NULL) OR (@Read = 0 AND n.[ReadDate] IS NULL), 1, 0) = 1) - OR (@Deleted IS NULL - OR IIF((@Deleted = 1 AND n.[DeletedDate] IS NOT NULL) OR - (@Deleted = 0 AND n.[DeletedDate] IS NULL), - 1, 0) = 1)))) + AND (@Deleted IS NULL + OR IIF((@Deleted = 1 AND n.[DeletedDate] IS NOT NULL) OR + (@Deleted = 0 AND n.[DeletedDate] IS NULL), + 1, 0) = 1))) ORDER BY [Priority] DESC, n.[CreationDate] DESC + OFFSET @PageSize * (@PageNumber - 1) ROWS FETCH NEXT @PageSize ROWS ONLY END diff --git a/test/Api.IntegrationTest/NotificationCenter/Controllers/NotificationsControllerTests.cs b/test/Api.IntegrationTest/NotificationCenter/Controllers/NotificationsControllerTests.cs new file mode 100644 index 0000000000..6d487c5d8f --- /dev/null +++ b/test/Api.IntegrationTest/NotificationCenter/Controllers/NotificationsControllerTests.cs @@ -0,0 +1,582 @@ +using System.Net; +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.Helpers; +using Bit.Api.Models.Response; +using Bit.Api.NotificationCenter.Models.Response; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Enums; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Api; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Enums; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Repositories; +using Xunit; + +namespace Bit.Api.IntegrationTest.NotificationCenter.Controllers; + +public class NotificationsControllerTests : IClassFixture, IAsyncLifetime +{ + private static readonly string _mockEncryptedBody = + "2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk="; + + private static readonly string _mockEncryptedTitle = + "2.06CDSJjTZaigYHUuswIq5A==|trxgZl2RCkYrrmCvGE9WNA==|w5p05eI5wsaYeSyWtsAPvBX63vj798kIMxBTfSB0BQg="; + + private static readonly Random _random = new(); + + private static TimeSpan OneMinuteTimeSpan => TimeSpan.FromMinutes(1); + + private readonly HttpClient _client; + private readonly ApiApplicationFactory _factory; + private readonly LoginHelper _loginHelper; + private readonly INotificationRepository _notificationRepository; + private readonly INotificationStatusRepository _notificationStatusRepository; + private readonly IUserRepository _userRepository; + private Organization _organization = null!; + private OrganizationUser _organizationUserOwner = null!; + private string _ownerEmail = null!; + private List<(Notification, NotificationStatus?)> _notificationsWithStatuses = null!; + + public NotificationsControllerTests(ApiApplicationFactory factory) + { + _factory = factory; + _client = factory.CreateClient(); + _loginHelper = new LoginHelper(_factory, _client); + _notificationRepository = _factory.GetService(); + _notificationStatusRepository = _factory.GetService(); + _userRepository = _factory.GetService(); + } + + public async Task InitializeAsync() + { + // Create the owner account + _ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(_ownerEmail); + + // Create the organization + (_organization, _organizationUserOwner) = await OrganizationTestHelpers.SignUpAsync(_factory, + plan: PlanType.EnterpriseAnnually, ownerEmail: _ownerEmail, passwordManagerSeats: 10, + paymentMethod: PaymentMethodType.Card); + + _notificationsWithStatuses = await CreateNotificationsWithStatusesAsync(); + } + + public Task DisposeAsync() + { + _client.Dispose(); + + foreach (var (notification, _) in _notificationsWithStatuses) + { + _notificationRepository.DeleteAsync(notification); + } + + return Task.CompletedTask; + } + + [Theory] + [InlineData("invalid")] + [InlineData("-1")] + [InlineData("0")] + public async Task ListAsync_RequestValidationContinuationInvalidNumber_BadRequest(string continuationToken) + { + await _loginHelper.LoginAsync(_ownerEmail); + + var response = await _client.GetAsync($"/notifications?continuationToken={continuationToken}"); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var result = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(result); + Assert.Contains("ContinuationToken", result.ValidationErrors); + Assert.Contains("Continuation token must be a positive, non zero integer.", + result.ValidationErrors["ContinuationToken"]); + } + + [Fact] + public async Task ListAsync_RequestValidationContinuationTokenMaxLengthExceeded_BadRequest() + { + await _loginHelper.LoginAsync(_ownerEmail); + + var response = await _client.GetAsync("/notifications?continuationToken=1234567890"); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var result = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(result); + Assert.Contains("ContinuationToken", result.ValidationErrors); + Assert.Contains("The field ContinuationToken must be a string with a maximum length of 9.", + result.ValidationErrors["ContinuationToken"]); + } + + [Theory] + [InlineData("9")] + [InlineData("1001")] + public async Task ListAsync_RequestValidationPageSizeInvalidRange_BadRequest(string pageSize) + { + await _loginHelper.LoginAsync(_ownerEmail); + + var response = await _client.GetAsync($"/notifications?pageSize={pageSize}"); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var result = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(result); + Assert.Contains("PageSize", result.ValidationErrors); + Assert.Contains("The field PageSize must be between 10 and 1000.", + result.ValidationErrors["PageSize"]); + } + + [Fact] + public async Task ListAsync_NotLoggedIn_Unauthorized() + { + var response = await _client.GetAsync("/notifications"); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Theory] + [InlineData(null, null, "2", 10)] + [InlineData(10, null, "2", 10)] + [InlineData(10, 2, "3", 10)] + [InlineData(10, 3, null, 0)] + [InlineData(15, null, "2", 15)] + [InlineData(15, 2, null, 5)] + [InlineData(20, null, "2", 20)] + [InlineData(20, 2, null, 0)] + [InlineData(1000, null, null, 20)] + public async Task ListAsync_PaginationFilter_ReturnsNextPageOfNotificationsCorrectOrder( + int? pageSize, int? pageNumber, string? expectedContinuationToken, int expectedCount) + { + var pageSizeWithDefault = pageSize ?? 10; + + await _loginHelper.LoginAsync(_ownerEmail); + + var skip = pageNumber == null ? 0 : (pageNumber.Value - 1) * pageSizeWithDefault; + + var notificationsInOrder = _notificationsWithStatuses.OrderByDescending(e => e.Item1.Priority) + .ThenByDescending(e => e.Item1.CreationDate) + .Skip(skip) + .Take(pageSizeWithDefault) + .ToList(); + + var url = "/notifications"; + if (pageNumber != null) + { + url += $"?continuationToken={pageNumber}"; + } + + if (pageSize != null) + { + url += url.Contains('?') ? "&" : "?"; + url += $"pageSize={pageSize}"; + } + + var response = await _client.GetAsync(url); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var result = await response.Content.ReadFromJsonAsync>(); + Assert.NotNull(result?.Data); + Assert.InRange(result.Data.Count(), 0, pageSizeWithDefault); + Assert.Equal(expectedCount, notificationsInOrder.Count); + Assert.Equal(notificationsInOrder.Count, result.Data.Count()); + AssertNotificationResponseModels(result.Data, notificationsInOrder); + + Assert.Equal(expectedContinuationToken, result.ContinuationToken); + } + + [Theory] + [InlineData(null, null)] + [InlineData(null, false)] + [InlineData(null, true)] + [InlineData(false, null)] + [InlineData(true, null)] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task ListAsync_ReadStatusDeletedStatusFilter_ReturnsFilteredNotificationsCorrectOrder( + bool? readStatusFilter, bool? deletedStatusFilter) + { + await _loginHelper.LoginAsync(_ownerEmail); + var notificationsInOrder = _notificationsWithStatuses.FindAll(e => + (readStatusFilter == null || readStatusFilter == (e.Item2?.ReadDate != null)) && + (deletedStatusFilter == null || deletedStatusFilter == (e.Item2?.DeletedDate != null))) + .OrderByDescending(e => e.Item1.Priority) + .ThenByDescending(e => e.Item1.CreationDate) + .Take(10) + .ToList(); + + var url = "/notifications"; + if (readStatusFilter != null) + { + url += $"?readStatusFilter={readStatusFilter}"; + } + + if (deletedStatusFilter != null) + { + url += url.Contains('?') ? "&" : "?"; + url += $"deletedStatusFilter={deletedStatusFilter}"; + } + + var response = await _client.GetAsync(url); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var result = await response.Content.ReadFromJsonAsync>(); + Assert.NotNull(result?.Data); + Assert.InRange(result.Data.Count(), 0, 10); + Assert.Equal(notificationsInOrder.Count, result.Data.Count()); + AssertNotificationResponseModels(result.Data, notificationsInOrder); + } + + [Fact] + private async void MarkAsDeletedAsync_NotLoggedIn_Unauthorized() + { + var url = $"/notifications/{Guid.NewGuid().ToString()}/delete"; + var response = await _client.PatchAsync(url, new StringContent("")); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + private async void MarkAsDeletedAsync_NonExistentNotificationId_NotFound() + { + await _loginHelper.LoginAsync(_ownerEmail); + + var url = $"/notifications/{Guid.NewGuid()}/delete"; + var response = await _client.PatchAsync(url, new StringContent("")); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + private async void MarkAsDeletedAsync_UserIdNotMatching_NotFound() + { + var email = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(email); + var user = (await _userRepository.GetByEmailAsync(email))!; + var notifications = await CreateNotificationsAsync(user.Id); + + await _loginHelper.LoginAsync(_ownerEmail); + + var url = $"/notifications/{notifications[0].Id}/delete"; + var response = await _client.PatchAsync(url, new StringContent("")); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + private async void MarkAsDeletedAsync_OrganizationIdNotMatchingUserNotPartOfOrganization_NotFound() + { + var email = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(email); + var user = (await _userRepository.GetByEmailAsync(email))!; + var notifications = await CreateNotificationsAsync(user.Id, _organization.Id); + + await _loginHelper.LoginAsync(email); + + var url = $"/notifications/{notifications[0].Id}/delete"; + var response = await _client.PatchAsync(url, new StringContent("")); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + private async void MarkAsDeletedAsync_OrganizationIdNotMatchingUserPartOfDifferentOrganization_NotFound() + { + var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, + plan: PlanType.EnterpriseAnnually, ownerEmail: _ownerEmail, passwordManagerSeats: 10, + paymentMethod: PaymentMethodType.Card); + var email = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(email); + var user = (await _userRepository.GetByEmailAsync(email))!; + await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, email, OrganizationUserType.User); + var notifications = await CreateNotificationsAsync(user.Id, _organization.Id); + + await _loginHelper.LoginAsync(email); + + var url = $"/notifications/{notifications[0].Id}/delete"; + var response = await _client.PatchAsync(url, new StringContent("")); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + private async void MarkAsDeletedAsync_NotificationStatusNotExisting_Created() + { + var notifications = await CreateNotificationsAsync(_organizationUserOwner.UserId); + + await _loginHelper.LoginAsync(_ownerEmail); + + var url = $"/notifications/{notifications[0].Id}/delete"; + var response = await _client.PatchAsync(url, new StringContent("")); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync( + notifications[0].Id, _organizationUserOwner.UserId!.Value); + Assert.NotNull(notificationStatus); + Assert.NotNull(notificationStatus.DeletedDate); + Assert.Equal(DateTime.UtcNow, notificationStatus.DeletedDate.Value, OneMinuteTimeSpan); + Assert.Null(notificationStatus.ReadDate); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + private async void MarkAsDeletedAsync_NotificationStatusExisting_Updated(bool deletedDateNull) + { + var notifications = await CreateNotificationsAsync(_organizationUserOwner.UserId); + await _notificationStatusRepository.CreateAsync(new NotificationStatus + { + NotificationId = notifications[0].Id, + UserId = _organizationUserOwner.UserId!.Value, + ReadDate = null, + DeletedDate = deletedDateNull ? null : DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600)) + }); + + await _loginHelper.LoginAsync(_ownerEmail); + + var url = $"/notifications/{notifications[0].Id}/delete"; + var response = await _client.PatchAsync(url, new StringContent("")); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync( + notifications[0].Id, _organizationUserOwner.UserId!.Value); + Assert.NotNull(notificationStatus); + Assert.NotNull(notificationStatus.DeletedDate); + Assert.Equal(DateTime.UtcNow, notificationStatus.DeletedDate.Value, OneMinuteTimeSpan); + Assert.Null(notificationStatus.ReadDate); + } + + [Fact] + private async void MarkAsReadAsync_NotLoggedIn_Unauthorized() + { + var url = $"/notifications/{Guid.NewGuid().ToString()}/read"; + var response = await _client.PatchAsync(url, new StringContent("")); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + private async void MarkAsReadAsync_NonExistentNotificationId_NotFound() + { + await _loginHelper.LoginAsync(_ownerEmail); + + var url = $"/notifications/{Guid.NewGuid()}/read"; + var response = await _client.PatchAsync(url, new StringContent("")); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + private async void MarkAsReadAsync_UserIdNotMatching_NotFound() + { + var email = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(email); + var user = (await _userRepository.GetByEmailAsync(email))!; + var notifications = await CreateNotificationsAsync(user.Id); + + await _loginHelper.LoginAsync(_ownerEmail); + + var url = $"/notifications/{notifications[0].Id}/read"; + var response = await _client.PatchAsync(url, new StringContent("")); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + private async void MarkAsReadAsync_OrganizationIdNotMatchingUserNotPartOfOrganization_NotFound() + { + var email = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(email); + var user = (await _userRepository.GetByEmailAsync(email))!; + var notifications = await CreateNotificationsAsync(user.Id, _organization.Id); + + await _loginHelper.LoginAsync(email); + + var url = $"/notifications/{notifications[0].Id}/read"; + var response = await _client.PatchAsync(url, new StringContent("")); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + private async void MarkAsReadAsync_OrganizationIdNotMatchingUserPartOfDifferentOrganization_NotFound() + { + var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, + plan: PlanType.EnterpriseAnnually, ownerEmail: _ownerEmail, passwordManagerSeats: 10, + paymentMethod: PaymentMethodType.Card); + var email = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(email); + var user = (await _userRepository.GetByEmailAsync(email))!; + await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, email, OrganizationUserType.User); + var notifications = await CreateNotificationsAsync(user.Id, _organization.Id); + + await _loginHelper.LoginAsync(email); + + var url = $"/notifications/{notifications[0].Id}/read"; + var response = await _client.PatchAsync(url, new StringContent("")); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + private async void MarkAsReadAsync_NotificationStatusNotExisting_Created() + { + var notifications = await CreateNotificationsAsync(_organizationUserOwner.UserId); + + await _loginHelper.LoginAsync(_ownerEmail); + + var url = $"/notifications/{notifications[0].Id}/read"; + var response = await _client.PatchAsync(url, new StringContent("")); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync( + notifications[0].Id, _organizationUserOwner.UserId!.Value); + Assert.NotNull(notificationStatus); + Assert.NotNull(notificationStatus.ReadDate); + Assert.Equal(DateTime.UtcNow, notificationStatus.ReadDate.Value, OneMinuteTimeSpan); + Assert.Null(notificationStatus.DeletedDate); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + private async void MarkAsReadAsync_NotificationStatusExisting_Updated(bool readDateNull) + { + var notifications = await CreateNotificationsAsync(_organizationUserOwner.UserId); + await _notificationStatusRepository.CreateAsync(new NotificationStatus + { + NotificationId = notifications[0].Id, + UserId = _organizationUserOwner.UserId!.Value, + ReadDate = readDateNull ? null : DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600)), + DeletedDate = null + }); + + await _loginHelper.LoginAsync(_ownerEmail); + + var url = $"/notifications/{notifications[0].Id}/read"; + var response = await _client.PatchAsync(url, new StringContent("")); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync( + notifications[0].Id, _organizationUserOwner.UserId!.Value); + Assert.NotNull(notificationStatus); + Assert.NotNull(notificationStatus.ReadDate); + Assert.Equal(DateTime.UtcNow, notificationStatus.ReadDate.Value, OneMinuteTimeSpan); + Assert.Null(notificationStatus.DeletedDate); + } + + private static void AssertNotificationResponseModels( + IEnumerable notificationResponseModels, + List<(Notification, NotificationStatus?)> expectedNotificationsWithStatuses) + { + var i = 0; + foreach (var notificationResponseModel in notificationResponseModels) + { + Assert.Contains(expectedNotificationsWithStatuses, e => e.Item1.Id == notificationResponseModel.Id); + var (expectedNotification, expectedNotificationStatus) = expectedNotificationsWithStatuses[i]; + Assert.NotNull(expectedNotification); + Assert.Equal(expectedNotification.Priority, notificationResponseModel.Priority); + Assert.Equal(expectedNotification.Title, notificationResponseModel.Title); + Assert.Equal(expectedNotification.Body, notificationResponseModel.Body); + Assert.Equal(expectedNotification.RevisionDate, notificationResponseModel.Date); + if (expectedNotificationStatus != null) + { + Assert.Equal(expectedNotificationStatus.ReadDate, notificationResponseModel.ReadDate); + Assert.Equal(expectedNotificationStatus.DeletedDate, notificationResponseModel.DeletedDate); + } + else + { + Assert.Null(notificationResponseModel.ReadDate); + Assert.Null(notificationResponseModel.DeletedDate); + } + + Assert.Equal("notification", notificationResponseModel.Object); + i++; + } + } + + private async Task> CreateNotificationsWithStatusesAsync() + { + var userId = (Guid)_organizationUserOwner.UserId!; + + var globalNotifications = await CreateNotificationsAsync(); + var userWithoutOrganizationNotifications = await CreateNotificationsAsync(userId: userId); + var organizationWithoutUserNotifications = await CreateNotificationsAsync(organizationId: _organization.Id); + var userPartOrOrganizationNotifications = await CreateNotificationsAsync(userId: userId, + organizationId: _organization.Id); + + var globalNotificationWithStatuses = await CreateNotificationStatusesAsync(globalNotifications, userId); + var userWithoutOrganizationNotificationWithStatuses = + await CreateNotificationStatusesAsync(userWithoutOrganizationNotifications, userId); + var organizationWithoutUserNotificationWithStatuses = + await CreateNotificationStatusesAsync(organizationWithoutUserNotifications, userId); + var userPartOrOrganizationNotificationWithStatuses = + await CreateNotificationStatusesAsync(userPartOrOrganizationNotifications, userId); + + return new List> + { + globalNotificationWithStatuses, + userWithoutOrganizationNotificationWithStatuses, + organizationWithoutUserNotificationWithStatuses, + userPartOrOrganizationNotificationWithStatuses + } + .SelectMany(n => n) + .ToList(); + } + + private async Task> CreateNotificationsAsync(Guid? userId = null, Guid? organizationId = null, + int numberToCreate = 5) + { + var priorities = Enum.GetValues(); + var clientTypes = Enum.GetValues(); + + var notifications = new List(); + + foreach (var clientType in clientTypes) + { + for (var i = 0; i < numberToCreate; i++) + { + var notification = new Notification + { + Global = userId == null && organizationId == null, + UserId = userId, + OrganizationId = organizationId, + Title = _mockEncryptedTitle, + Body = _mockEncryptedBody, + Priority = (Priority)priorities.GetValue(_random.Next(priorities.Length))!, + ClientType = clientType, + CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600)), + RevisionDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600)) + }; + + notification = await _notificationRepository.CreateAsync(notification); + + notifications.Add(notification); + } + } + + return notifications; + } + + private async Task> CreateNotificationStatusesAsync( + List notifications, Guid userId) + { + var readDateNotificationStatus = await _notificationStatusRepository.CreateAsync(new NotificationStatus + { + NotificationId = notifications[0].Id, + UserId = userId, + ReadDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600)), + DeletedDate = null + }); + + var deletedDateNotificationStatus = await _notificationStatusRepository.CreateAsync(new NotificationStatus + { + NotificationId = notifications[1].Id, + UserId = userId, + ReadDate = null, + DeletedDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600)) + }); + + var readDateAndDeletedDateNotificationStatus = await _notificationStatusRepository.CreateAsync( + new NotificationStatus + { + NotificationId = notifications[2].Id, + UserId = userId, + ReadDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600)), + DeletedDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600)) + }); + + return + [ + (notifications[0], readDateNotificationStatus), + (notifications[1], deletedDateNotificationStatus), + (notifications[2], readDateAndDeletedDateNotificationStatus), + (notifications[3], null), + (notifications[4], null) + ]; + } +} diff --git a/test/Api.Test/NotificationCenter/Controllers/NotificationsControllerTests.cs b/test/Api.Test/NotificationCenter/Controllers/NotificationsControllerTests.cs new file mode 100644 index 0000000000..b8b21ef419 --- /dev/null +++ b/test/Api.Test/NotificationCenter/Controllers/NotificationsControllerTests.cs @@ -0,0 +1,202 @@ +#nullable enable +using Bit.Api.NotificationCenter.Controllers; +using Bit.Api.NotificationCenter.Models.Request; +using Bit.Core.Models.Data; +using Bit.Core.NotificationCenter.Commands.Interfaces; +using Bit.Core.NotificationCenter.Models.Data; +using Bit.Core.NotificationCenter.Models.Filter; +using Bit.Core.NotificationCenter.Queries.Interfaces; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.NotificationCenter.Controllers; + +[ControllerCustomize(typeof(NotificationsController))] +[SutProviderCustomize] +public class NotificationsControllerTests +{ + [Theory] + [BitAutoData([null, null])] + [BitAutoData([null, false])] + [BitAutoData([null, true])] + [BitAutoData(false, null)] + [BitAutoData(true, null)] + [BitAutoData(false, false)] + [BitAutoData(false, true)] + [BitAutoData(true, false)] + [BitAutoData(true, true)] + [NotificationStatusDetailsListCustomize(5)] + public async Task ListAsync_StatusFilter_ReturnedMatchingNotifications(bool? readStatusFilter, bool? deletedStatusFilter, + SutProvider sutProvider, + IEnumerable notificationStatusDetailsEnumerable) + { + var notificationStatusDetailsList = notificationStatusDetailsEnumerable + .OrderByDescending(n => n.Priority) + .ThenByDescending(n => n.CreationDate) + .ToList(); + + sutProvider.GetDependency() + .GetByUserIdStatusFilterAsync(Arg.Any(), Arg.Any()) + .Returns(new PagedResult { Data = notificationStatusDetailsList }); + + var expectedNotificationStatusDetailsMap = notificationStatusDetailsList + .Take(10) + .ToDictionary(n => n.Id); + + var listResponse = await sutProvider.Sut.ListAsync(new NotificationFilterRequestModel + { + ReadStatusFilter = readStatusFilter, + DeletedStatusFilter = deletedStatusFilter + }); + + Assert.Equal("list", listResponse.Object); + Assert.Equal(5, listResponse.Data.Count()); + Assert.All(listResponse.Data, notificationResponseModel => + { + Assert.Equal("notification", notificationResponseModel.Object); + Assert.True(expectedNotificationStatusDetailsMap.ContainsKey(notificationResponseModel.Id)); + var expectedNotificationStatusDetails = expectedNotificationStatusDetailsMap[notificationResponseModel.Id]; + Assert.NotNull(expectedNotificationStatusDetails); + Assert.Equal(expectedNotificationStatusDetails.Id, notificationResponseModel.Id); + Assert.Equal(expectedNotificationStatusDetails.Priority, notificationResponseModel.Priority); + Assert.Equal(expectedNotificationStatusDetails.Title, notificationResponseModel.Title); + Assert.Equal(expectedNotificationStatusDetails.Body, notificationResponseModel.Body); + Assert.Equal(expectedNotificationStatusDetails.RevisionDate, notificationResponseModel.Date); + Assert.Equal(expectedNotificationStatusDetails.ReadDate, notificationResponseModel.ReadDate); + Assert.Equal(expectedNotificationStatusDetails.DeletedDate, notificationResponseModel.DeletedDate); + }); + Assert.Null(listResponse.ContinuationToken); + + await sutProvider.GetDependency() + .Received(1) + .GetByUserIdStatusFilterAsync(Arg.Is(filter => + filter.Read == readStatusFilter && filter.Deleted == deletedStatusFilter), + Arg.Is(pageOptions => + pageOptions.ContinuationToken == null && pageOptions.PageSize == 10)); + } + + [Theory] + [BitAutoData] + [NotificationStatusDetailsListCustomize(19)] + public async Task ListAsync_PagingRequestNoContinuationToken_ReturnedFirst10MatchingNotifications( + SutProvider sutProvider, + IEnumerable notificationStatusDetailsEnumerable) + { + var notificationStatusDetailsList = notificationStatusDetailsEnumerable + .OrderByDescending(n => n.Priority) + .ThenByDescending(n => n.CreationDate) + .ToList(); + + sutProvider.GetDependency() + .GetByUserIdStatusFilterAsync(Arg.Any(), Arg.Any()) + .Returns(new PagedResult + { Data = notificationStatusDetailsList.Take(10).ToList(), ContinuationToken = "2" }); + + var expectedNotificationStatusDetailsMap = notificationStatusDetailsList + .Take(10) + .ToDictionary(n => n.Id); + + var listResponse = await sutProvider.Sut.ListAsync(new NotificationFilterRequestModel()); + + Assert.Equal("list", listResponse.Object); + Assert.Equal(10, listResponse.Data.Count()); + Assert.All(listResponse.Data, notificationResponseModel => + { + Assert.Equal("notification", notificationResponseModel.Object); + Assert.True(expectedNotificationStatusDetailsMap.ContainsKey(notificationResponseModel.Id)); + var expectedNotificationStatusDetails = expectedNotificationStatusDetailsMap[notificationResponseModel.Id]; + Assert.NotNull(expectedNotificationStatusDetails); + Assert.Equal(expectedNotificationStatusDetails.Id, notificationResponseModel.Id); + Assert.Equal(expectedNotificationStatusDetails.Priority, notificationResponseModel.Priority); + Assert.Equal(expectedNotificationStatusDetails.Title, notificationResponseModel.Title); + Assert.Equal(expectedNotificationStatusDetails.Body, notificationResponseModel.Body); + Assert.Equal(expectedNotificationStatusDetails.RevisionDate, notificationResponseModel.Date); + Assert.Equal(expectedNotificationStatusDetails.ReadDate, notificationResponseModel.ReadDate); + Assert.Equal(expectedNotificationStatusDetails.DeletedDate, notificationResponseModel.DeletedDate); + }); + Assert.Equal("2", listResponse.ContinuationToken); + + await sutProvider.GetDependency() + .Received(1) + .GetByUserIdStatusFilterAsync(Arg.Any(), + Arg.Is(pageOptions => + pageOptions.ContinuationToken == null && pageOptions.PageSize == 10)); + } + + [Theory] + [BitAutoData] + [NotificationStatusDetailsListCustomize(19)] + public async Task ListAsync_PagingRequestUsingContinuationToken_ReturnedLast9MatchingNotifications( + SutProvider sutProvider, + IEnumerable notificationStatusDetailsEnumerable) + { + var notificationStatusDetailsList = notificationStatusDetailsEnumerable + .OrderByDescending(n => n.Priority) + .ThenByDescending(n => n.CreationDate) + .ToList(); + + sutProvider.GetDependency() + .GetByUserIdStatusFilterAsync(Arg.Any(), Arg.Any()) + .Returns(new PagedResult + { Data = notificationStatusDetailsList.Skip(10).ToList() }); + + var expectedNotificationStatusDetailsMap = notificationStatusDetailsList + .Skip(10) + .ToDictionary(n => n.Id); + + var listResponse = await sutProvider.Sut.ListAsync(new NotificationFilterRequestModel { ContinuationToken = "2" }); + + Assert.Equal("list", listResponse.Object); + Assert.Equal(9, listResponse.Data.Count()); + Assert.All(listResponse.Data, notificationResponseModel => + { + Assert.Equal("notification", notificationResponseModel.Object); + Assert.True(expectedNotificationStatusDetailsMap.ContainsKey(notificationResponseModel.Id)); + var expectedNotificationStatusDetails = expectedNotificationStatusDetailsMap[notificationResponseModel.Id]; + Assert.NotNull(expectedNotificationStatusDetails); + Assert.Equal(expectedNotificationStatusDetails.Id, notificationResponseModel.Id); + Assert.Equal(expectedNotificationStatusDetails.Priority, notificationResponseModel.Priority); + Assert.Equal(expectedNotificationStatusDetails.Title, notificationResponseModel.Title); + Assert.Equal(expectedNotificationStatusDetails.Body, notificationResponseModel.Body); + Assert.Equal(expectedNotificationStatusDetails.RevisionDate, notificationResponseModel.Date); + Assert.Equal(expectedNotificationStatusDetails.ReadDate, notificationResponseModel.ReadDate); + Assert.Equal(expectedNotificationStatusDetails.DeletedDate, notificationResponseModel.DeletedDate); + }); + Assert.Null(listResponse.ContinuationToken); + + await sutProvider.GetDependency() + .Received(1) + .GetByUserIdStatusFilterAsync(Arg.Any(), + Arg.Is(pageOptions => + pageOptions.ContinuationToken == "2" && pageOptions.PageSize == 10)); + } + + [Theory] + [BitAutoData] + public async Task MarkAsDeletedAsync_NotificationId_MarkedAsDeleted( + SutProvider sutProvider, + Guid notificationId) + { + await sutProvider.Sut.MarkAsDeletedAsync(notificationId); + + await sutProvider.GetDependency() + .Received(1) + .MarkDeletedAsync(notificationId); + } + + [Theory] + [BitAutoData] + public async Task MarkAsReadAsync_NotificationId_MarkedAsRead( + SutProvider sutProvider, + Guid notificationId) + { + await sutProvider.Sut.MarkAsReadAsync(notificationId); + + await sutProvider.GetDependency() + .Received(1) + .MarkReadAsync(notificationId); + } +} diff --git a/test/Api.Test/NotificationCenter/Models/Request/NotificationFilterRequestModelTests.cs b/test/Api.Test/NotificationCenter/Models/Request/NotificationFilterRequestModelTests.cs new file mode 100644 index 0000000000..8b72d13e71 --- /dev/null +++ b/test/Api.Test/NotificationCenter/Models/Request/NotificationFilterRequestModelTests.cs @@ -0,0 +1,93 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using Bit.Api.NotificationCenter.Models.Request; +using Xunit; + +namespace Bit.Api.Test.NotificationCenter.Models.Request; + +public class NotificationFilterRequestModelTests +{ + [Theory] + [InlineData("invalid")] + [InlineData("-1")] + [InlineData("0")] + public void Validate_ContinuationTokenInvalidNumber_Invalid(string continuationToken) + { + var model = new NotificationFilterRequestModel + { + ContinuationToken = continuationToken, + }; + var result = Validate(model); + Assert.Single(result); + Assert.Contains("Continuation token must be a positive, non zero integer.", result[0].ErrorMessage); + Assert.Contains("ContinuationToken", result[0].MemberNames); + } + + [Fact] + public void Validate_ContinuationTokenMaxLengthExceeded_Invalid() + { + var model = new NotificationFilterRequestModel + { + ContinuationToken = "1234567890" + }; + var result = Validate(model); + Assert.Single(result); + Assert.Contains("The field ContinuationToken must be a string with a maximum length of 9.", + result[0].ErrorMessage); + Assert.Contains("ContinuationToken", result[0].MemberNames); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("1")] + [InlineData("123456789")] + public void Validate_ContinuationTokenCorrect_Valid(string? continuationToken) + { + var model = new NotificationFilterRequestModel + { + ContinuationToken = continuationToken + }; + var result = Validate(model); + Assert.Empty(result); + } + + [Theory] + [InlineData(9)] + [InlineData(1001)] + public void Validate_PageSizeInvalidRange_Invalid(int pageSize) + { + var model = new NotificationFilterRequestModel + { + PageSize = pageSize + }; + var result = Validate(model); + Assert.Single(result); + Assert.Contains("The field PageSize must be between 10 and 1000.", result[0].ErrorMessage); + Assert.Contains("PageSize", result[0].MemberNames); + } + + [Theory] + [InlineData(null)] + [InlineData(10)] + [InlineData(1000)] + public void Validate_PageSizeCorrect_Valid(int? pageSize) + { + var model = pageSize == null + ? new NotificationFilterRequestModel() + : new NotificationFilterRequestModel + { + PageSize = pageSize.Value + }; + var result = Validate(model); + Assert.Empty(result); + } + + private static List Validate(NotificationFilterRequestModel model) + { + var results = new List(); + Validator.TryValidateObject(model, new ValidationContext(model), results, true); + return results; + } +} diff --git a/test/Api.Test/NotificationCenter/Models/Response/NotificationResponseModelTests.cs b/test/Api.Test/NotificationCenter/Models/Response/NotificationResponseModelTests.cs new file mode 100644 index 0000000000..f0dfc03fec --- /dev/null +++ b/test/Api.Test/NotificationCenter/Models/Response/NotificationResponseModelTests.cs @@ -0,0 +1,43 @@ +#nullable enable +using Bit.Api.NotificationCenter.Models.Response; +using Bit.Core.Enums; +using Bit.Core.NotificationCenter.Enums; +using Bit.Core.NotificationCenter.Models.Data; +using Xunit; + +namespace Bit.Api.Test.NotificationCenter.Models.Response; + +public class NotificationResponseModelTests +{ + [Fact] + public void Constructor_NotificationStatusDetailsNull_CorrectFields() + { + Assert.Throws(() => new NotificationResponseModel(null!)); + } + + [Fact] + public void Constructor_NotificationStatusDetails_CorrectFields() + { + var notificationStatusDetails = new NotificationStatusDetails + { + Id = Guid.NewGuid(), + Global = true, + Priority = Priority.High, + ClientType = ClientType.All, + Title = "Test Title", + Body = "Test Body", + RevisionDate = DateTime.UtcNow - TimeSpan.FromMinutes(3), + ReadDate = DateTime.UtcNow - TimeSpan.FromMinutes(1), + DeletedDate = DateTime.UtcNow, + }; + var model = new NotificationResponseModel(notificationStatusDetails); + + Assert.Equal(model.Id, notificationStatusDetails.Id); + Assert.Equal(model.Priority, notificationStatusDetails.Priority); + Assert.Equal(model.Title, notificationStatusDetails.Title); + Assert.Equal(model.Body, notificationStatusDetails.Body); + Assert.Equal(model.Date, notificationStatusDetails.RevisionDate); + Assert.Equal(model.ReadDate, notificationStatusDetails.ReadDate); + Assert.Equal(model.DeletedDate, notificationStatusDetails.DeletedDate); + } +} diff --git a/test/Core.Test/NotificationCenter/AutoFixture/NotificationStatusDetailsFixtures.cs b/test/Core.Test/NotificationCenter/AutoFixture/NotificationStatusDetailsFixtures.cs index 1e1d066d16..71c9878f42 100644 --- a/test/Core.Test/NotificationCenter/AutoFixture/NotificationStatusDetailsFixtures.cs +++ b/test/Core.Test/NotificationCenter/AutoFixture/NotificationStatusDetailsFixtures.cs @@ -9,9 +9,32 @@ public class NotificationStatusDetailsCustomization : ICustomization { public void Customize(IFixture fixture) { - fixture.Customize(composer => composer.With(n => n.Id, Guid.NewGuid()) - .With(n => n.UserId, Guid.NewGuid()) - .With(n => n.OrganizationId, Guid.NewGuid())); + fixture.Customize(composer => + { + return composer.With(n => n.Id, Guid.NewGuid()) + .With(n => n.UserId, Guid.NewGuid()) + .With(n => n.OrganizationId, Guid.NewGuid()); + }); + } +} + +public class NotificationStatusDetailsListCustomization(int count) : ICustomization +{ + public void Customize(IFixture fixture) + { + var customization = new NotificationStatusDetailsCustomization(); + fixture.Customize>(composer => composer.FromFactory(() => + { + var notifications = new List(); + for (var i = 0; i < count; i++) + { + customization.Customize(fixture); + var notificationStatusDetails = fixture.Create(); + notifications.Add(notificationStatusDetails); + } + + return notifications; + })); } } @@ -19,3 +42,8 @@ public class NotificationStatusDetailsCustomizeAttribute : BitCustomizeAttribute { public override ICustomization GetCustomization() => new NotificationStatusDetailsCustomization(); } + +public class NotificationStatusDetailsListCustomizeAttribute(int count) : BitCustomizeAttribute +{ + public override ICustomization GetCustomization() => new NotificationStatusDetailsListCustomization(count); +} diff --git a/test/Core.Test/NotificationCenter/Queries/GetNotificationStatusDetailsForUserQueryTest.cs b/test/Core.Test/NotificationCenter/Queries/GetNotificationStatusDetailsForUserQueryTest.cs index 7d9c265606..d0c89a45d9 100644 --- a/test/Core.Test/NotificationCenter/Queries/GetNotificationStatusDetailsForUserQueryTest.cs +++ b/test/Core.Test/NotificationCenter/Queries/GetNotificationStatusDetailsForUserQueryTest.cs @@ -2,6 +2,7 @@ using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Data; using Bit.Core.NotificationCenter.Models.Data; using Bit.Core.NotificationCenter.Models.Filter; using Bit.Core.NotificationCenter.Queries; @@ -19,37 +20,49 @@ namespace Bit.Core.Test.NotificationCenter.Queries; public class GetNotificationStatusDetailsForUserQueryTest { private static void Setup(SutProvider sutProvider, - List notificationsStatusDetails, NotificationStatusFilter statusFilter, Guid? userId) + List notificationsStatusDetails, NotificationStatusFilter statusFilter, Guid? userId, + PageOptions pageOptions, string? continuationToken) { sutProvider.GetDependency().UserId.Returns(userId); - sutProvider.GetDependency().GetByUserIdAndStatusAsync( - userId.GetValueOrDefault(Guid.NewGuid()), Arg.Any(), statusFilter) - .Returns(notificationsStatusDetails); + sutProvider.GetDependency() + .GetByUserIdAndStatusAsync(userId.GetValueOrDefault(Guid.NewGuid()), Arg.Any(), statusFilter, + pageOptions) + .Returns(new PagedResult + { + Data = notificationsStatusDetails, + ContinuationToken = continuationToken + }); } [Theory] [BitAutoData] public async Task GetByUserIdStatusFilterAsync_NotLoggedIn_NotFoundException( SutProvider sutProvider, - List notificationsStatusDetails, NotificationStatusFilter notificationStatusFilter) + List notificationsStatusDetails, NotificationStatusFilter notificationStatusFilter, + PageOptions pageOptions, string? continuationToken) { - Setup(sutProvider, notificationsStatusDetails, notificationStatusFilter, userId: null); + Setup(sutProvider, notificationsStatusDetails, notificationStatusFilter, userId: null, pageOptions, + continuationToken); await Assert.ThrowsAsync(() => - sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter)); + sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter, pageOptions)); } [Theory] [BitAutoData] public async Task GetByUserIdStatusFilterAsync_NotificationsFound_Returned( SutProvider sutProvider, - List notificationsStatusDetails, NotificationStatusFilter notificationStatusFilter) + List notificationsStatusDetails, NotificationStatusFilter notificationStatusFilter, + PageOptions pageOptions, string? continuationToken) { - Setup(sutProvider, notificationsStatusDetails, notificationStatusFilter, Guid.NewGuid()); + Setup(sutProvider, notificationsStatusDetails, notificationStatusFilter, Guid.NewGuid(), pageOptions, + continuationToken); - var actualNotificationsStatusDetails = - await sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter); + var actualNotificationsStatusDetailsPagedResult = + await sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter, pageOptions); - Assert.Equal(notificationsStatusDetails, actualNotificationsStatusDetails); + Assert.NotNull(actualNotificationsStatusDetailsPagedResult); + Assert.Equal(notificationsStatusDetails, actualNotificationsStatusDetailsPagedResult.Data); + Assert.Equal(continuationToken, actualNotificationsStatusDetailsPagedResult.ContinuationToken); } } diff --git a/util/Migrator/DbScripts/2024-12-18_00_AddPagingToNotificationRead.sql b/util/Migrator/DbScripts/2024-12-18_00_AddPagingToNotificationRead.sql new file mode 100644 index 0000000000..21e19c193c --- /dev/null +++ b/util/Migrator/DbScripts/2024-12-18_00_AddPagingToNotificationRead.sql @@ -0,0 +1,39 @@ +-- Stored Procedure Notification_ReadByUserIdAndStatus + +CREATE OR ALTER PROCEDURE [dbo].[Notification_ReadByUserIdAndStatus] + @UserId UNIQUEIDENTIFIER, + @ClientType TINYINT, + @Read BIT, + @Deleted BIT, + @PageNumber INT = 1, + @PageSize INT = 10 +AS +BEGIN + SET NOCOUNT ON + + SELECT n.* + FROM [dbo].[NotificationStatusDetailsView] n + LEFT JOIN [dbo].[OrganizationUserView] ou ON n.[OrganizationId] = ou.[OrganizationId] + AND ou.[UserId] = @UserId + WHERE (n.[NotificationStatusUserId] IS NULL OR n.[NotificationStatusUserId] = @UserId) + AND [ClientType] IN (0, CASE WHEN @ClientType != 0 THEN @ClientType END) + AND ([Global] = 1 + OR (n.[UserId] = @UserId + AND (n.[OrganizationId] IS NULL + OR ou.[OrganizationId] IS NOT NULL)) + OR (n.[UserId] IS NULL + AND ou.[OrganizationId] IS NOT NULL)) + AND ((@Read IS NULL AND @Deleted IS NULL) + OR (n.[NotificationStatusUserId] IS NOT NULL + AND (@Read IS NULL + OR IIF((@Read = 1 AND n.[ReadDate] IS NOT NULL) OR + (@Read = 0 AND n.[ReadDate] IS NULL), + 1, 0) = 1) + AND (@Deleted IS NULL + OR IIF((@Deleted = 1 AND n.[DeletedDate] IS NOT NULL) OR + (@Deleted = 0 AND n.[DeletedDate] IS NULL), + 1, 0) = 1))) + ORDER BY [Priority] DESC, n.[CreationDate] DESC + OFFSET @PageSize * (@PageNumber - 1) ROWS FETCH NEXT @PageSize ROWS ONLY +END +GO From 322a07477a27c759e5b637662cde0a8001ffef80 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:31:07 +0100 Subject: [PATCH 92/94] organization status changed code changes (#5113) * organization status changed code changes Signed-off-by: Cy Okeke * Add the push notification to subscriptionUpdated Signed-off-by: Cy Okeke * send notification using the SendPayloadToUser Signed-off-by: Cy Okeke * Change the implementation to send userId * Added new implementation for orgstatus sync * refactor the code and remove private methods --------- Signed-off-by: Cy Okeke --- .../Implementations/PaymentSucceededHandler.cs | 6 +++++- .../Implementations/SubscriptionUpdatedHandler.cs | 11 ++++++++++- src/Core/Enums/PushType.cs | 1 + src/Core/Models/PushNotification.cs | 6 ++++++ .../NotificationHubPushNotificationService.cs | 12 ++++++++++++ src/Core/Services/IPushNotificationService.cs | 4 +++- .../AzureQueuePushNotificationService.cs | 12 ++++++++++++ .../MultiServicePushNotificationService.cs | 9 ++++++++- .../NotificationsApiPushNotificationService.cs | 14 +++++++++++++- .../RelayPushNotificationService.cs | 14 +++++++++++++- .../NoopPushNotificationService.cs | 8 +++++++- src/Notifications/HubHelpers.cs | 7 +++++++ 12 files changed, 97 insertions(+), 7 deletions(-) diff --git a/src/Billing/Services/Implementations/PaymentSucceededHandler.cs b/src/Billing/Services/Implementations/PaymentSucceededHandler.cs index 6aa8aa2b9f..49578187f9 100644 --- a/src/Billing/Services/Implementations/PaymentSucceededHandler.cs +++ b/src/Billing/Services/Implementations/PaymentSucceededHandler.cs @@ -25,6 +25,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler private readonly ICurrentContext _currentContext; private readonly IUserRepository _userRepository; private readonly IStripeEventUtilityService _stripeEventUtilityService; + private readonly IPushNotificationService _pushNotificationService; public PaymentSucceededHandler( ILogger logger, @@ -37,7 +38,8 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler IUserRepository userRepository, IStripeEventUtilityService stripeEventUtilityService, IUserService userService, - IOrganizationService organizationService) + IOrganizationService organizationService, + IPushNotificationService pushNotificationService) { _logger = logger; _stripeEventService = stripeEventService; @@ -50,6 +52,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler _stripeEventUtilityService = stripeEventUtilityService; _userService = userService; _organizationService = organizationService; + _pushNotificationService = pushNotificationService; } /// @@ -140,6 +143,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler await _organizationService.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd); var organization = await _organizationRepository.GetByIdAsync(organizationId.Value); + await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); await _referenceEventService.RaiseEventAsync( new ReferenceEvent(ReferenceEventType.Rebilled, organization, _currentContext) diff --git a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs index 4b4c9dcf4a..d49b22b7fb 100644 --- a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs @@ -1,5 +1,6 @@ using Bit.Billing.Constants; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; using Stripe; @@ -15,6 +16,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler private readonly IStripeFacade _stripeFacade; private readonly IOrganizationSponsorshipRenewCommand _organizationSponsorshipRenewCommand; private readonly IUserService _userService; + private readonly IPushNotificationService _pushNotificationService; + private readonly IOrganizationRepository _organizationRepository; public SubscriptionUpdatedHandler( IStripeEventService stripeEventService, @@ -22,7 +25,9 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler IOrganizationService organizationService, IStripeFacade stripeFacade, IOrganizationSponsorshipRenewCommand organizationSponsorshipRenewCommand, - IUserService userService) + IUserService userService, + IPushNotificationService pushNotificationService, + IOrganizationRepository organizationRepository) { _stripeEventService = stripeEventService; _stripeEventUtilityService = stripeEventUtilityService; @@ -30,6 +35,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler _stripeFacade = stripeFacade; _organizationSponsorshipRenewCommand = organizationSponsorshipRenewCommand; _userService = userService; + _pushNotificationService = pushNotificationService; + _organizationRepository = organizationRepository; } /// @@ -70,6 +77,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler case StripeSubscriptionStatus.Active when organizationId.HasValue: { await _organizationService.EnableAsync(organizationId.Value); + var organization = await _organizationRepository.GetByIdAsync(organizationId.Value); + await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); break; } case StripeSubscriptionStatus.Active: diff --git a/src/Core/Enums/PushType.cs b/src/Core/Enums/PushType.cs index 9dbef7b8e2..2030b855e2 100644 --- a/src/Core/Enums/PushType.cs +++ b/src/Core/Enums/PushType.cs @@ -25,4 +25,5 @@ public enum PushType : byte AuthRequestResponse = 16, SyncOrganizations = 17, + SyncOrganizationStatusChanged = 18, } diff --git a/src/Core/Models/PushNotification.cs b/src/Core/Models/PushNotification.cs index 37b3b25c0d..667080580e 100644 --- a/src/Core/Models/PushNotification.cs +++ b/src/Core/Models/PushNotification.cs @@ -50,3 +50,9 @@ public class AuthRequestPushNotification public Guid UserId { get; set; } public Guid Id { get; set; } } + +public class OrganizationStatusPushNotification +{ + public Guid OrganizationId { get; set; } + public bool Enabled { get; set; } +} diff --git a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs index 6143676def..7438e812e0 100644 --- a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.RegularExpressions; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Context; using Bit.Core.Enums; @@ -226,6 +227,17 @@ public class NotificationHubPushNotificationService : IPushNotificationService } } + public async Task PushSyncOrganizationStatusAsync(Organization organization) + { + var message = new OrganizationStatusPushNotification + { + OrganizationId = organization.Id, + Enabled = organization.Enabled + }; + + await SendPayloadToOrganizationAsync(organization.Id, PushType.SyncOrganizationStatusChanged, message, false); + } + private string GetContextIdentifier(bool excludeCurrentContext) { if (!excludeCurrentContext) diff --git a/src/Core/Services/IPushNotificationService.cs b/src/Core/Services/IPushNotificationService.cs index 29a20239d1..6e2e47e27f 100644 --- a/src/Core/Services/IPushNotificationService.cs +++ b/src/Core/Services/IPushNotificationService.cs @@ -1,4 +1,5 @@ -using Bit.Core.Auth.Entities; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Entities; using Bit.Core.Enums; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; @@ -27,4 +28,5 @@ public interface IPushNotificationService Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, string deviceId = null); Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier, string deviceId = null); + Task PushSyncOrganizationStatusAsync(Organization organization); } diff --git a/src/Core/Services/Implementations/AzureQueuePushNotificationService.cs b/src/Core/Services/Implementations/AzureQueuePushNotificationService.cs index 1e4a7314c4..3daadebf3a 100644 --- a/src/Core/Services/Implementations/AzureQueuePushNotificationService.cs +++ b/src/Core/Services/Implementations/AzureQueuePushNotificationService.cs @@ -1,5 +1,6 @@ using System.Text.Json; using Azure.Storage.Queues; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Context; using Bit.Core.Enums; @@ -221,4 +222,15 @@ public class AzureQueuePushNotificationService : IPushNotificationService // Noop return Task.FromResult(0); } + + public async Task PushSyncOrganizationStatusAsync(Organization organization) + { + var message = new OrganizationStatusPushNotification + { + OrganizationId = organization.Id, + Enabled = organization.Enabled + }; + await SendMessageAsync(PushType.SyncOrganizationStatusChanged, message, false); + } + } diff --git a/src/Core/Services/Implementations/MultiServicePushNotificationService.cs b/src/Core/Services/Implementations/MultiServicePushNotificationService.cs index 00be72c980..185a11adbb 100644 --- a/src/Core/Services/Implementations/MultiServicePushNotificationService.cs +++ b/src/Core/Services/Implementations/MultiServicePushNotificationService.cs @@ -1,4 +1,5 @@ -using Bit.Core.Auth.Entities; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Entities; using Bit.Core.Enums; using Bit.Core.Settings; using Bit.Core.Tools.Entities; @@ -144,6 +145,12 @@ public class MultiServicePushNotificationService : IPushNotificationService return Task.FromResult(0); } + public Task PushSyncOrganizationStatusAsync(Organization organization) + { + PushToServices((s) => s.PushSyncOrganizationStatusAsync(organization)); + return Task.FromResult(0); + } + private void PushToServices(Func pushFunc) { if (_services != null) diff --git a/src/Core/Services/Implementations/NotificationsApiPushNotificationService.cs b/src/Core/Services/Implementations/NotificationsApiPushNotificationService.cs index 9ec1eb31d4..feec75fbe0 100644 --- a/src/Core/Services/Implementations/NotificationsApiPushNotificationService.cs +++ b/src/Core/Services/Implementations/NotificationsApiPushNotificationService.cs @@ -1,4 +1,5 @@ -using Bit.Core.Auth.Entities; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Entities; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models; @@ -227,4 +228,15 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService // Noop return Task.FromResult(0); } + + public async Task PushSyncOrganizationStatusAsync(Organization organization) + { + var message = new OrganizationStatusPushNotification + { + OrganizationId = organization.Id, + Enabled = organization.Enabled + }; + + await SendMessageAsync(PushType.SyncOrganizationStatusChanged, message, false); + } } diff --git a/src/Core/Services/Implementations/RelayPushNotificationService.cs b/src/Core/Services/Implementations/RelayPushNotificationService.cs index 6cfc0c0a61..d725296779 100644 --- a/src/Core/Services/Implementations/RelayPushNotificationService.cs +++ b/src/Core/Services/Implementations/RelayPushNotificationService.cs @@ -1,4 +1,5 @@ -using Bit.Core.Auth.Entities; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Entities; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.IdentityServer; @@ -251,4 +252,15 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti { throw new NotImplementedException(); } + + public async Task PushSyncOrganizationStatusAsync(Organization organization) + { + var message = new OrganizationStatusPushNotification + { + OrganizationId = organization.Id, + Enabled = organization.Enabled + }; + + await SendPayloadToOrganizationAsync(organization.Id, PushType.SyncOrganizationStatusChanged, message, false); + } } diff --git a/src/Core/Services/NoopImplementations/NoopPushNotificationService.cs b/src/Core/Services/NoopImplementations/NoopPushNotificationService.cs index d4eff93ef6..b5e2616220 100644 --- a/src/Core/Services/NoopImplementations/NoopPushNotificationService.cs +++ b/src/Core/Services/NoopImplementations/NoopPushNotificationService.cs @@ -1,4 +1,5 @@ -using Bit.Core.Auth.Entities; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Entities; using Bit.Core.Enums; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; @@ -88,6 +89,11 @@ public class NoopPushNotificationService : IPushNotificationService return Task.FromResult(0); } + public Task PushSyncOrganizationStatusAsync(Organization organization) + { + return Task.FromResult(0); + } + public Task PushAuthRequestAsync(AuthRequest authRequest) { return Task.FromResult(0); diff --git a/src/Notifications/HubHelpers.cs b/src/Notifications/HubHelpers.cs index 53edb76389..ce2e6b24ad 100644 --- a/src/Notifications/HubHelpers.cs +++ b/src/Notifications/HubHelpers.cs @@ -85,6 +85,13 @@ public static class HubHelpers await hubContext.Clients.User(authRequestNotification.Payload.UserId.ToString()) .SendAsync("ReceiveMessage", authRequestNotification, cancellationToken); break; + case PushType.SyncOrganizationStatusChanged: + var orgStatusNotification = + JsonSerializer.Deserialize>( + notificationJson, _deserializerOptions); + await hubContext.Clients.Group($"Organization_{orgStatusNotification.Payload.OrganizationId}") + .SendAsync("ReceiveMessage", orgStatusNotification, cancellationToken); + break; default: break; } From 4f50461521b329d97986b5b18bebbe20c0bd4a2b Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Wed, 18 Dec 2024 16:00:43 -0500 Subject: [PATCH 93/94] Remove link to missing file (#5166) --- bitwarden-server.sln | 1 - 1 file changed, 1 deletion(-) diff --git a/bitwarden-server.sln b/bitwarden-server.sln index ad643c43c3..75e7d7fade 100644 --- a/bitwarden-server.sln +++ b/bitwarden-server.sln @@ -18,7 +18,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig TRADEMARK_GUIDELINES.md = TRADEMARK_GUIDELINES.md SECURITY.md = SECURITY.md - NuGet.Config = NuGet.Config LICENSE_FAQ.md = LICENSE_FAQ.md LICENSE_BITWARDEN.txt = LICENSE_BITWARDEN.txt LICENSE_AGPL.txt = LICENSE_AGPL.txt From 1962e8bede22f54bcb1245cffd8a507b7e754264 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Wed, 18 Dec 2024 22:44:31 +0000 Subject: [PATCH 94/94] PM-10600: Broken NotificationsController integration test Device type is now part of JWT access token, so the notification center results in the integration test are now scoped to client type web and all. --- .../NotificationsControllerTests.cs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/test/Api.IntegrationTest/NotificationCenter/Controllers/NotificationsControllerTests.cs b/test/Api.IntegrationTest/NotificationCenter/Controllers/NotificationsControllerTests.cs index 6d487c5d8f..ca04c9775d 100644 --- a/test/Api.IntegrationTest/NotificationCenter/Controllers/NotificationsControllerTests.cs +++ b/test/Api.IntegrationTest/NotificationCenter/Controllers/NotificationsControllerTests.cs @@ -133,12 +133,10 @@ public class NotificationsControllerTests : IClassFixture [InlineData(null, null, "2", 10)] [InlineData(10, null, "2", 10)] [InlineData(10, 2, "3", 10)] - [InlineData(10, 3, null, 0)] - [InlineData(15, null, "2", 15)] - [InlineData(15, 2, null, 5)] - [InlineData(20, null, "2", 20)] - [InlineData(20, 2, null, 0)] - [InlineData(1000, null, null, 20)] + [InlineData(10, 3, null, 4)] + [InlineData(24, null, "2", 24)] + [InlineData(24, 2, null, 0)] + [InlineData(1000, null, null, 24)] public async Task ListAsync_PaginationFilter_ReturnsNextPageOfNotificationsCorrectOrder( int? pageSize, int? pageNumber, string? expectedContinuationToken, int expectedCount) { @@ -505,11 +503,12 @@ public class NotificationsControllerTests : IClassFixture userPartOrOrganizationNotificationWithStatuses } .SelectMany(n => n) + .Where(n => n.Item1.ClientType is ClientType.All or ClientType.Web) .ToList(); } private async Task> CreateNotificationsAsync(Guid? userId = null, Guid? organizationId = null, - int numberToCreate = 5) + int numberToCreate = 3) { var priorities = Enum.GetValues(); var clientTypes = Enum.GetValues(); @@ -570,13 +569,9 @@ public class NotificationsControllerTests : IClassFixture DeletedDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600)) }); - return - [ - (notifications[0], readDateNotificationStatus), - (notifications[1], deletedDateNotificationStatus), - (notifications[2], readDateAndDeletedDateNotificationStatus), - (notifications[3], null), - (notifications[4], null) - ]; + List statuses = + [readDateNotificationStatus, deletedDateNotificationStatus, readDateAndDeletedDateNotificationStatus]; + + return notifications.Select(n => (n, statuses.Find(s => s.NotificationId == n.Id))).ToList(); } }