From 8a68f075ccf65345cbe679296cef5a55443bb9b7 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 10 Jan 2025 11:55:40 +0100 Subject: [PATCH 001/148] Remove block legacy users feature flag (#5212) --- src/Core/Constants.cs | 1 - .../RequestValidators/BaseRequestValidator.cs | 9 +++------ .../IdentityServer/BaseRequestValidatorTests.cs | 1 - 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index defcc41e93..f06f63573d 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -115,7 +115,6 @@ public static class FeatureFlagKeys public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service"; public const string VaultBulkManagementAction = "vault-bulk-management-action"; public const string MemberAccessReport = "ac-2059-member-access-report"; - public const string BlockLegacyUsers = "block-legacy-users"; public const string InlineMenuFieldQualification = "inline-menu-field-qualification"; public const string TwoFactorComponentRefactor = "two-factor-component-refactor"; public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements"; diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index 78c00f86d5..ea207a7aaa 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -183,13 +183,10 @@ public abstract class BaseRequestValidator where T : class } // 5. Force legacy users to the web for migration - if (FeatureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers)) + if (UserService.IsLegacyUser(user) && request.ClientId != "web") { - if (UserService.IsLegacyUser(user) && request.ClientId != "web") - { - await FailAuthForLegacyUserAsync(user, context); - return; - } + await FailAuthForLegacyUserAsync(user, context); + return; } await BuildSuccessResultAsync(user, context, validatorContext.Device, returnRememberMeToken); diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index 02b6982419..916b52e1d0 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -369,7 +369,6 @@ public class BaseRequestValidatorTests context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; context.ValidatedTokenRequest.ClientId = "Not Web"; _sut.isValid = true; - _featureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers).Returns(true); _twoFactorAuthenticationValidator .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new Tuple(false, null))); From fbfabf2651276242b35e06706657d19a5a0450bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:45:09 +0000 Subject: [PATCH 002/148] [PM-15547] Fix two-factor authentication revocation logic and update related tests (#5246) * Fix two-factor authentication revocation logic and update related tests * Refine test for RevokeNonCompliantOrganizationUserCommand to assert single user revocation --- .../Services/Implementations/UserService.cs | 5 +- ...onCompliantOrganizationUserCommandTests.cs | 2 +- test/Core.Test/Services/UserServiceTests.cs | 71 ++++++++++++++++--- 3 files changed, 63 insertions(+), 15 deletions(-) diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 4944dfe9e7..78da7b42e3 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1372,17 +1372,16 @@ 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 => { var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId); - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && organizationsManagingUser.Any(o => o.Id == p.OrganizationId)) + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) { await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync( new RevokeOrganizationUsersRequest( p.OrganizationId, - [new OrganizationUserUserDetails { UserId = user.Id, OrganizationId = p.OrganizationId }], + [new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }], new SystemUser(EventSystemUser.TwoFactorDisabled))); await _mailService.SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(organization.DisplayName(), user.Email); } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommandTests.cs index 3653cd27d7..0ccad9e5c7 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommandTests.cs @@ -162,7 +162,7 @@ public class RevokeNonCompliantOrganizationUserCommandTests await sutProvider.GetDependency() .Received(1) - .RevokeManyByIdAsync(Arg.Any>()); + .RevokeManyByIdAsync(Arg.Is>(x => x.Count() == 1 && x.Contains(userToRevoke.Id))); Assert.True(result.Success); diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index a07cc1907f..d8a0ade1fa 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -454,8 +454,10 @@ public class UserServiceTests } [Theory, BitAutoData] - public async Task DisableTwoFactorProviderAsync_WithAccountDeprovisioningEnabled_WhenOrganizationHas2FAPolicyEnabled_WhenUserIsManaged_DisablingAllProviders_RemovesOrRevokesUserAndSendsEmail( - SutProvider sutProvider, User user, Organization organization1, Organization organization2) + public async Task DisableTwoFactorProviderAsync_WithAccountDeprovisioningEnabled_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RevokesUserAndSendsEmail( + SutProvider sutProvider, User user, + Organization organization1, Guid organizationUserId1, + Organization organization2, Guid organizationUserId2) { // Arrange user.SetTwoFactorProviders(new Dictionary @@ -464,6 +466,7 @@ public class UserServiceTests }); organization1.Enabled = organization2.Enabled = true; organization1.UseSso = organization2.UseSso = true; + sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) .Returns(true); @@ -474,12 +477,14 @@ public class UserServiceTests new OrganizationUserPolicyDetails { OrganizationId = organization1.Id, + OrganizationUserId = organizationUserId1, PolicyType = PolicyType.TwoFactorAuthentication, PolicyEnabled = true }, new OrganizationUserPolicyDetails { OrganizationId = organization2.Id, + OrganizationUserId = organizationUserId2, PolicyType = PolicyType.TwoFactorAuthentication, PolicyEnabled = true } @@ -490,9 +495,6 @@ public class UserServiceTests sutProvider.GetDependency() .GetByIdAsync(organization2.Id) .Returns(organization2); - sutProvider.GetDependency() - .GetByVerifiedUserEmailDomainAsync(user.Id) - .Returns(new[] { organization1 }); var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary(), JsonHelpers.LegacyEnumKeyResolver); // Act @@ -506,24 +508,71 @@ public class UserServiceTests .Received(1) .LogUserEventAsync(user.Id, EventType.User_Disabled2fa); - // Revoke the user from the first organization because they are managed by it + // Revoke the user from the first organization await sutProvider.GetDependency() .Received(1) .RevokeNonCompliantOrganizationUsersAsync( Arg.Is(r => r.OrganizationId == organization1.Id && - r.OrganizationUsers.First().UserId == user.Id && + r.OrganizationUsers.First().Id == organizationUserId1 && 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() + // Remove the user from the second organization + await sutProvider.GetDependency() .Received(1) - .RemoveUserAsync(organization2.Id, user.Id); + .RevokeNonCompliantOrganizationUsersAsync( + Arg.Is(r => r.OrganizationId == organization2.Id && + r.OrganizationUsers.First().Id == organizationUserId2 && + r.OrganizationUsers.First().OrganizationId == organization2.Id)); await sutProvider.GetDependency() .Received(1) - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization2.DisplayName(), user.Email); + .SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(organization2.DisplayName(), user.Email); + } + + [Theory, BitAutoData] + public async Task DisableTwoFactorProviderAsync_WithAccountDeprovisioningEnabled_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() + .DidNotReceiveWithAnyArgs() + .RevokeNonCompliantOrganizationUsersAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(default, default); } [Theory, BitAutoData] From 45d2c5315d7b629268b01d03340bcf0cc7c7f86c Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Fri, 10 Jan 2025 16:39:02 +0100 Subject: [PATCH 003/148] [PM-14894] Drop Tax Rate tables - Stage 1 (#5236) --- src/Admin/Controllers/ToolsController.cs | 163 -------- src/Admin/Models/TaxRateAddEditModel.cs | 10 - src/Admin/Models/TaxRatesModel.cs | 8 - src/Admin/Views/Shared/_Layout.cshtml | 9 +- src/Admin/Views/Tools/TaxRate.cshtml | 127 ------- src/Admin/Views/Tools/TaxRateAddEdit.cshtml | 356 ------------------ src/Api/Controllers/PlansController.cs | 13 - .../Models/Response/TaxRateResponseModel.cs | 28 -- src/Billing/Services/IStripeFacade.cs | 6 - .../Services/Implementations/StripeFacade.cs | 8 - .../Business/SubscriptionCreateOptions.cs | 5 - src/Core/Models/Business/TaxInfo.cs | 1 - src/Core/Repositories/ITaxRateRepository.cs | 13 - src/Core/Services/IPaymentService.cs | 3 - src/Core/Services/IStripeAdapter.cs | 3 - .../Services/Implementations/StripeAdapter.cs | 12 - .../Implementations/StripePaymentService.cs | 48 --- .../DapperServiceCollectionExtensions.cs | 1 - .../Repositories/TaxRateRepository.cs | 70 ---- ...ityFrameworkServiceCollectionExtensions.cs | 1 - .../Repositories/TaxRateRepository.cs | 68 ---- .../AutoFixture/TaxRateFixtures.cs | 56 --- .../Repositories/TaxRateRepositoryTests.cs | 39 -- 23 files changed, 1 insertion(+), 1047 deletions(-) delete mode 100644 src/Admin/Models/TaxRateAddEditModel.cs delete mode 100644 src/Admin/Models/TaxRatesModel.cs delete mode 100644 src/Admin/Views/Tools/TaxRate.cshtml delete mode 100644 src/Admin/Views/Tools/TaxRateAddEdit.cshtml delete mode 100644 src/Api/Models/Response/TaxRateResponseModel.cs delete mode 100644 src/Core/Repositories/ITaxRateRepository.cs delete mode 100644 src/Infrastructure.Dapper/Repositories/TaxRateRepository.cs delete mode 100644 src/Infrastructure.EntityFramework/Repositories/TaxRateRepository.cs delete mode 100644 test/Infrastructure.EFIntegration.Test/AutoFixture/TaxRateFixtures.cs delete mode 100644 test/Infrastructure.EFIntegration.Test/Repositories/TaxRateRepositoryTests.cs diff --git a/src/Admin/Controllers/ToolsController.cs b/src/Admin/Controllers/ToolsController.cs index 45319cf79c..a84fb681e2 100644 --- a/src/Admin/Controllers/ToolsController.cs +++ b/src/Admin/Controllers/ToolsController.cs @@ -16,7 +16,6 @@ using Bit.Core.Settings; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using TaxRate = Bit.Core.Entities.TaxRate; namespace Bit.Admin.Controllers; @@ -33,7 +32,6 @@ public class ToolsController : Controller private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IProviderUserRepository _providerUserRepository; private readonly IPaymentService _paymentService; - private readonly ITaxRateRepository _taxRateRepository; private readonly IStripeAdapter _stripeAdapter; private readonly IWebHostEnvironment _environment; @@ -46,7 +44,6 @@ public class ToolsController : Controller IInstallationRepository installationRepository, IOrganizationUserRepository organizationUserRepository, IProviderUserRepository providerUserRepository, - ITaxRateRepository taxRateRepository, IPaymentService paymentService, IStripeAdapter stripeAdapter, IWebHostEnvironment environment) @@ -59,7 +56,6 @@ public class ToolsController : Controller _installationRepository = installationRepository; _organizationUserRepository = organizationUserRepository; _providerUserRepository = providerUserRepository; - _taxRateRepository = taxRateRepository; _paymentService = paymentService; _stripeAdapter = stripeAdapter; _environment = environment; @@ -346,165 +342,6 @@ public class ToolsController : Controller } } - [RequirePermission(Permission.Tools_ManageTaxRates)] - public async Task TaxRate(int page = 1, int count = 25) - { - if (page < 1) - { - page = 1; - } - - if (count < 1) - { - count = 1; - } - - var skip = (page - 1) * count; - var rates = await _taxRateRepository.SearchAsync(skip, count); - return View(new TaxRatesModel - { - Items = rates.ToList(), - Page = page, - Count = count - }); - } - - [RequirePermission(Permission.Tools_ManageTaxRates)] - public async Task TaxRateAddEdit(string stripeTaxRateId = null) - { - if (string.IsNullOrWhiteSpace(stripeTaxRateId)) - { - return View(new TaxRateAddEditModel()); - } - - var rate = await _taxRateRepository.GetByIdAsync(stripeTaxRateId); - var model = new TaxRateAddEditModel() - { - StripeTaxRateId = stripeTaxRateId, - Country = rate.Country, - State = rate.State, - PostalCode = rate.PostalCode, - Rate = rate.Rate - }; - - return View(model); - } - - [ValidateAntiForgeryToken] - [RequirePermission(Permission.Tools_ManageTaxRates)] - public async Task TaxRateUpload(IFormFile file) - { - if (file == null || file.Length == 0) - { - throw new ArgumentNullException(nameof(file)); - } - - // Build rates and validate them first before updating DB & Stripe - var taxRateUpdates = new List(); - var currentTaxRates = await _taxRateRepository.GetAllActiveAsync(); - using var reader = new StreamReader(file.OpenReadStream()); - while (!reader.EndOfStream) - { - var line = await reader.ReadLineAsync(); - if (string.IsNullOrWhiteSpace(line)) - { - continue; - } - var taxParts = line.Split(','); - if (taxParts.Length < 2) - { - throw new Exception($"This line is not in the format of ,,,: {line}"); - } - var postalCode = taxParts[0].Trim(); - if (string.IsNullOrWhiteSpace(postalCode)) - { - throw new Exception($"'{line}' is not valid, the first element must contain a postal code."); - } - if (!decimal.TryParse(taxParts[1], out var rate) || rate <= 0M || rate > 100) - { - throw new Exception($"{taxParts[1]} is not a valid rate/decimal for {postalCode}"); - } - var state = taxParts.Length > 2 ? taxParts[2] : null; - var country = (taxParts.Length > 3 ? taxParts[3] : null); - if (string.IsNullOrWhiteSpace(country)) - { - country = "US"; - } - var taxRate = currentTaxRates.FirstOrDefault(r => r.Country == country && r.PostalCode == postalCode) ?? - new TaxRate - { - Country = country, - PostalCode = postalCode, - Active = true, - }; - taxRate.Rate = rate; - taxRate.State = state ?? taxRate.State; - taxRateUpdates.Add(taxRate); - } - - foreach (var taxRate in taxRateUpdates) - { - if (!string.IsNullOrWhiteSpace(taxRate.Id)) - { - await _paymentService.UpdateTaxRateAsync(taxRate); - } - else - { - await _paymentService.CreateTaxRateAsync(taxRate); - } - } - - return RedirectToAction("TaxRate"); - } - - [HttpPost] - [ValidateAntiForgeryToken] - [RequirePermission(Permission.Tools_ManageTaxRates)] - public async Task TaxRateAddEdit(TaxRateAddEditModel model) - { - var existingRateCheck = await _taxRateRepository.GetByLocationAsync(new TaxRate() { Country = model.Country, PostalCode = model.PostalCode }); - if (existingRateCheck.Any()) - { - ModelState.AddModelError(nameof(model.PostalCode), "A tax rate already exists for this Country/Postal Code combination."); - } - - if (!ModelState.IsValid) - { - return View(model); - } - - var taxRate = new TaxRate() - { - Id = model.StripeTaxRateId, - Country = model.Country, - State = model.State, - PostalCode = model.PostalCode, - Rate = model.Rate - }; - - if (!string.IsNullOrWhiteSpace(model.StripeTaxRateId)) - { - await _paymentService.UpdateTaxRateAsync(taxRate); - } - else - { - await _paymentService.CreateTaxRateAsync(taxRate); - } - - return RedirectToAction("TaxRate"); - } - - [RequirePermission(Permission.Tools_ManageTaxRates)] - public async Task TaxRateArchive(string stripeTaxRateId) - { - if (!string.IsNullOrWhiteSpace(stripeTaxRateId)) - { - await _paymentService.ArchiveTaxRateAsync(new TaxRate() { Id = stripeTaxRateId }); - } - - return RedirectToAction("TaxRate"); - } - [RequirePermission(Permission.Tools_ManageStripeSubscriptions)] public async Task StripeSubscriptions(StripeSubscriptionListOptions options) { diff --git a/src/Admin/Models/TaxRateAddEditModel.cs b/src/Admin/Models/TaxRateAddEditModel.cs deleted file mode 100644 index bfa87d7cc8..0000000000 --- a/src/Admin/Models/TaxRateAddEditModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Bit.Admin.Models; - -public class TaxRateAddEditModel -{ - public string StripeTaxRateId { get; set; } - public string Country { get; set; } - public string State { get; set; } - public string PostalCode { get; set; } - public decimal Rate { get; set; } -} diff --git a/src/Admin/Models/TaxRatesModel.cs b/src/Admin/Models/TaxRatesModel.cs deleted file mode 100644 index 0af073f384..0000000000 --- a/src/Admin/Models/TaxRatesModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Bit.Core.Entities; - -namespace Bit.Admin.Models; - -public class TaxRatesModel : PagedModel -{ - public string Message { get; set; } -} diff --git a/src/Admin/Views/Shared/_Layout.cshtml b/src/Admin/Views/Shared/_Layout.cshtml index 939eb86b86..361c1f9a57 100644 --- a/src/Admin/Views/Shared/_Layout.cshtml +++ b/src/Admin/Views/Shared/_Layout.cshtml @@ -16,13 +16,12 @@ 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 || canPromoteProviderServiceUser || - canGenerateLicense || canManageTaxRates || canManageStripeSubscriptions; + canGenerateLicense || canManageStripeSubscriptions; } @@ -107,12 +106,6 @@ Generate License } - @if (canManageTaxRates) - { - - Manage Tax Rates - - } @if (canManageStripeSubscriptions) { diff --git a/src/Admin/Views/Tools/TaxRate.cshtml b/src/Admin/Views/Tools/TaxRate.cshtml deleted file mode 100644 index 902390190c..0000000000 --- a/src/Admin/Views/Tools/TaxRate.cshtml +++ /dev/null @@ -1,127 +0,0 @@ -@model TaxRatesModel -@{ - ViewData["Title"] = "Tax Rates"; -} - -

Manage Tax Rates

- -

Bulk Upload Tax Rates

-
-

- Upload a CSV file containing multiple tax rates in bulk in order to update existing rates by country - and postal code OR to create new rates where a currently active rate is not found already. -

-

CSV Upload Format

-
    -
  • Postal Code (required) - The postal code for the tax rate.
  • -
  • Rate (required) - The effective tax rate for this postal code.
  • -
  • State (optional) - The ISO-2 character code for the state. Optional but recommended.
  • -
  • Country (optional) - The ISO-2 character country code, defaults to "US" if not provided.
  • -
-

Example (white-space is ignored):

-
-
-
87654,8.25,FL,US
-22334,8.5,CA
-11223,7
-
-
-
-
- -
-
- -
-
-
- -
-

View & Manage Tax Rates

-
Add a Rate -
- - - - - - - - - - - - - @if(!Model.Items.Any()) - { - - - - } - else - { - @foreach(var rate in Model.Items) - { - - - - - - - - - } - } - -
IdCountryStatePostal CodeTax Rate
No results to list.
- @{ - var taxRateToEdit = new Dictionary - { - { "id", rate.Id }, - { "stripeTaxRateId", rate.Id } - }; - } - @rate.Id - - @rate.Country - - @rate.State - - @rate.PostalCode - - @rate.Rate% - - - - -
-
- - diff --git a/src/Admin/Views/Tools/TaxRateAddEdit.cshtml b/src/Admin/Views/Tools/TaxRateAddEdit.cshtml deleted file mode 100644 index ea6bd15561..0000000000 --- a/src/Admin/Views/Tools/TaxRateAddEdit.cshtml +++ /dev/null @@ -1,356 +0,0 @@ -@model TaxRateAddEditModel -@{ - ViewData["Title"] = "Add/Edit Tax Rate"; -} - - -

@(string.IsNullOrWhiteSpace(Model.StripeTaxRateId) ? "Create" : "Edit") Tax Rate

- -@if (!string.IsNullOrWhiteSpace(Model.StripeTaxRateId)) -{ -

Note: Updating a Tax Rate archives the currently selected rate and creates a new rate with a new ID. The previous data still exists in a disabled state.

-} - -
-
- -
-
-
- - -
-
-
-
- - -
-
-
-
-
-
- - -
-
-
-
- -
- -
- % -
-
-
-
-
- -
diff --git a/src/Api/Controllers/PlansController.cs b/src/Api/Controllers/PlansController.cs index 80aca2d827..c2ee494322 100644 --- a/src/Api/Controllers/PlansController.cs +++ b/src/Api/Controllers/PlansController.cs @@ -1,5 +1,4 @@ using Bit.Api.Models.Response; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -10,10 +9,6 @@ namespace Bit.Api.Controllers; [Authorize("Web")] public class PlansController : Controller { - private readonly ITaxRateRepository _taxRateRepository; - - public PlansController(ITaxRateRepository taxRateRepository) => _taxRateRepository = taxRateRepository; - [HttpGet("")] [AllowAnonymous] public ListResponseModel Get() @@ -21,12 +16,4 @@ public class PlansController : Controller var responses = StaticStore.Plans.Select(plan => new PlanResponseModel(plan)); return new ListResponseModel(responses); } - - [HttpGet("sales-tax-rates")] - public async Task> GetTaxRates() - { - var data = await _taxRateRepository.GetAllActiveAsync(); - var responses = data.Select(x => new TaxRateResponseModel(x)); - return new ListResponseModel(responses); - } } diff --git a/src/Api/Models/Response/TaxRateResponseModel.cs b/src/Api/Models/Response/TaxRateResponseModel.cs deleted file mode 100644 index 2c3335314c..0000000000 --- a/src/Api/Models/Response/TaxRateResponseModel.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Bit.Core.Entities; -using Bit.Core.Models.Api; - -namespace Bit.Api.Models.Response; - -public class TaxRateResponseModel : ResponseModel -{ - public TaxRateResponseModel(TaxRate taxRate) - : base("profile") - { - if (taxRate == null) - { - throw new ArgumentNullException(nameof(taxRate)); - } - - Id = taxRate.Id; - Country = taxRate.Country; - State = taxRate.State; - PostalCode = taxRate.PostalCode; - Rate = taxRate.Rate; - } - - public string Id { get; set; } - public string Country { get; set; } - public string State { get; set; } - public string PostalCode { get; set; } - public decimal Rate { get; set; } -} diff --git a/src/Billing/Services/IStripeFacade.cs b/src/Billing/Services/IStripeFacade.cs index f793846a53..77ba9a1ad4 100644 --- a/src/Billing/Services/IStripeFacade.cs +++ b/src/Billing/Services/IStripeFacade.cs @@ -80,12 +80,6 @@ public interface IStripeFacade RequestOptions requestOptions = null, CancellationToken cancellationToken = default); - Task GetTaxRate( - string taxRateId, - TaxRateGetOptions options = null, - RequestOptions requestOptions = null, - CancellationToken cancellationToken = default); - Task DeleteCustomerDiscount( string customerId, RequestOptions requestOptions = null, diff --git a/src/Billing/Services/Implementations/StripeFacade.cs b/src/Billing/Services/Implementations/StripeFacade.cs index 4204946781..91e0c1c33a 100644 --- a/src/Billing/Services/Implementations/StripeFacade.cs +++ b/src/Billing/Services/Implementations/StripeFacade.cs @@ -10,7 +10,6 @@ public class StripeFacade : IStripeFacade private readonly InvoiceService _invoiceService = new(); private readonly PaymentMethodService _paymentMethodService = new(); private readonly SubscriptionService _subscriptionService = new(); - private readonly TaxRateService _taxRateService = new(); private readonly DiscountService _discountService = new(); public async Task GetCharge( @@ -99,13 +98,6 @@ public class StripeFacade : IStripeFacade CancellationToken cancellationToken = default) => await _subscriptionService.CancelAsync(subscriptionId, options, requestOptions, cancellationToken); - public async Task GetTaxRate( - string taxRateId, - TaxRateGetOptions options = null, - RequestOptions requestOptions = null, - CancellationToken cancellationToken = default) => - await _taxRateService.GetAsync(taxRateId, options, requestOptions, cancellationToken); - public async Task DeleteCustomerDiscount( string customerId, RequestOptions requestOptions = null, diff --git a/src/Core/Models/Business/SubscriptionCreateOptions.cs b/src/Core/Models/Business/SubscriptionCreateOptions.cs index 64626780ef..2d42ee66f7 100644 --- a/src/Core/Models/Business/SubscriptionCreateOptions.cs +++ b/src/Core/Models/Business/SubscriptionCreateOptions.cs @@ -34,11 +34,6 @@ public class OrganizationSubscriptionOptionsBase : SubscriptionCreateOptions AddPremiumAccessAddon(plan, premiumAccessAddon); AddPasswordManagerSeat(plan, additionalSeats); AddAdditionalStorage(plan, additionalStorageGb); - - if (!string.IsNullOrWhiteSpace(taxInfo?.StripeTaxRateId)) - { - DefaultTaxRates = new List { taxInfo.StripeTaxRateId }; - } } private void AddSecretsManagerSeat(Plan plan, int additionalSmSeats) diff --git a/src/Core/Models/Business/TaxInfo.cs b/src/Core/Models/Business/TaxInfo.cs index b12c5229b3..82a6ddfc3e 100644 --- a/src/Core/Models/Business/TaxInfo.cs +++ b/src/Core/Models/Business/TaxInfo.cs @@ -5,7 +5,6 @@ public class TaxInfo public string TaxIdNumber { get; set; } public string TaxIdType { get; set; } - public string StripeTaxRateId { get; set; } public string BillingAddressLine1 { get; set; } public string BillingAddressLine2 { get; set; } public string BillingAddressCity { get; set; } diff --git a/src/Core/Repositories/ITaxRateRepository.cs b/src/Core/Repositories/ITaxRateRepository.cs deleted file mode 100644 index c4d9e41238..0000000000 --- a/src/Core/Repositories/ITaxRateRepository.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Bit.Core.Entities; - -#nullable enable - -namespace Bit.Core.Repositories; - -public interface ITaxRateRepository : IRepository -{ - Task> SearchAsync(int skip, int count); - Task> GetAllActiveAsync(); - Task ArchiveAsync(TaxRate model); - Task> GetByLocationAsync(TaxRate taxRate); -} diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index 7d0f9d3c63..5bd2bede33 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -54,9 +54,6 @@ public interface IPaymentService Task GetSubscriptionAsync(ISubscriber subscriber); Task GetTaxInfoAsync(ISubscriber subscriber); Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo); - Task CreateTaxRateAsync(TaxRate taxRate); - Task UpdateTaxRateAsync(TaxRate taxRate); - Task ArchiveTaxRateAsync(TaxRate taxRate); Task AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats, int additionalServiceAccount); Task RisksSubscriptionFailure(Organization organization); diff --git a/src/Core/Services/IStripeAdapter.cs b/src/Core/Services/IStripeAdapter.cs index ef2e3ab766..cb95732a6e 100644 --- a/src/Core/Services/IStripeAdapter.cs +++ b/src/Core/Services/IStripeAdapter.cs @@ -43,9 +43,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); Task TaxIdDeleteAsync(string customerId, string taxIdId, Stripe.TaxIdDeleteOptions options = null); Task> ChargeListAsync(Stripe.ChargeListOptions options); diff --git a/src/Core/Services/Implementations/StripeAdapter.cs b/src/Core/Services/Implementations/StripeAdapter.cs index f4f8efe75f..f7f4fea066 100644 --- a/src/Core/Services/Implementations/StripeAdapter.cs +++ b/src/Core/Services/Implementations/StripeAdapter.cs @@ -9,7 +9,6 @@ public class StripeAdapter : IStripeAdapter private readonly Stripe.SubscriptionService _subscriptionService; private readonly Stripe.InvoiceService _invoiceService; private readonly Stripe.PaymentMethodService _paymentMethodService; - private readonly Stripe.TaxRateService _taxRateService; private readonly Stripe.TaxIdService _taxIdService; private readonly Stripe.ChargeService _chargeService; private readonly Stripe.RefundService _refundService; @@ -27,7 +26,6 @@ public class StripeAdapter : IStripeAdapter _subscriptionService = new Stripe.SubscriptionService(); _invoiceService = new Stripe.InvoiceService(); _paymentMethodService = new Stripe.PaymentMethodService(); - _taxRateService = new Stripe.TaxRateService(); _taxIdService = new Stripe.TaxIdService(); _chargeService = new Stripe.ChargeService(); _refundService = new Stripe.RefundService(); @@ -196,16 +194,6 @@ public class StripeAdapter : IStripeAdapter return _planService.GetAsync(id, options); } - public Task TaxRateCreateAsync(Stripe.TaxRateCreateOptions options) - { - return _taxRateService.CreateAsync(options); - } - - public Task TaxRateUpdateAsync(string id, Stripe.TaxRateUpdateOptions options) - { - return _taxRateService.UpdateAsync(id, options); - } - public Task TaxIdCreateAsync(string id, Stripe.TaxIdCreateOptions options) { return _taxIdService.CreateAsync(id, options); diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index ad8c7a599d..bf39085ea9 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -19,7 +19,6 @@ using Microsoft.Extensions.Logging; using Stripe; using PaymentMethod = Stripe.PaymentMethod; using StaticStore = Bit.Core.Models.StaticStore; -using TaxRate = Bit.Core.Entities.TaxRate; namespace Bit.Core.Services; @@ -33,7 +32,6 @@ public class StripePaymentService : IPaymentService private readonly ITransactionRepository _transactionRepository; private readonly ILogger _logger; private readonly Braintree.IBraintreeGateway _btGateway; - private readonly ITaxRateRepository _taxRateRepository; private readonly IStripeAdapter _stripeAdapter; private readonly IGlobalSettings _globalSettings; private readonly IFeatureService _featureService; @@ -43,7 +41,6 @@ public class StripePaymentService : IPaymentService public StripePaymentService( ITransactionRepository transactionRepository, ILogger logger, - ITaxRateRepository taxRateRepository, IStripeAdapter stripeAdapter, Braintree.IBraintreeGateway braintreeGateway, IGlobalSettings globalSettings, @@ -53,7 +50,6 @@ public class StripePaymentService : IPaymentService { _transactionRepository = transactionRepository; _logger = logger; - _taxRateRepository = taxRateRepository; _stripeAdapter = stripeAdapter; _btGateway = braintreeGateway; _globalSettings = globalSettings; @@ -1778,50 +1774,6 @@ public class StripePaymentService : IPaymentService } } - public async Task CreateTaxRateAsync(TaxRate taxRate) - { - var stripeTaxRateOptions = new TaxRateCreateOptions() - { - DisplayName = $"{taxRate.Country} - {taxRate.PostalCode}", - Inclusive = false, - Percentage = taxRate.Rate, - Active = true - }; - var stripeTaxRate = await _stripeAdapter.TaxRateCreateAsync(stripeTaxRateOptions); - taxRate.Id = stripeTaxRate.Id; - await _taxRateRepository.CreateAsync(taxRate); - return taxRate; - } - - public async Task UpdateTaxRateAsync(TaxRate taxRate) - { - if (string.IsNullOrWhiteSpace(taxRate.Id)) - { - return; - } - - await ArchiveTaxRateAsync(taxRate); - await CreateTaxRateAsync(taxRate); - } - - public async Task ArchiveTaxRateAsync(TaxRate taxRate) - { - if (string.IsNullOrWhiteSpace(taxRate.Id)) - { - return; - } - - var updatedStripeTaxRate = await _stripeAdapter.TaxRateUpdateAsync( - taxRate.Id, - new TaxRateUpdateOptions() { Active = false } - ); - if (!updatedStripeTaxRate.Active) - { - taxRate.Active = false; - await _taxRateRepository.ArchiveAsync(taxRate); - } - } - public async Task AddSecretsManagerToSubscription( Organization org, StaticStore.Plan plan, diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index 93814a6d7f..26abf5632c 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -51,7 +51,6 @@ public static class DapperServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Infrastructure.Dapper/Repositories/TaxRateRepository.cs b/src/Infrastructure.Dapper/Repositories/TaxRateRepository.cs deleted file mode 100644 index be60017262..0000000000 --- a/src/Infrastructure.Dapper/Repositories/TaxRateRepository.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Data; -using Bit.Core.Entities; -using Bit.Core.Repositories; -using Bit.Core.Settings; -using Dapper; -using Microsoft.Data.SqlClient; - -#nullable enable - -namespace Bit.Infrastructure.Dapper.Repositories; - -public class TaxRateRepository : Repository, ITaxRateRepository -{ - public TaxRateRepository(GlobalSettings globalSettings) - : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) - { } - - public TaxRateRepository(string connectionString, string readOnlyConnectionString) - : base(connectionString, readOnlyConnectionString) - { } - - public async Task> SearchAsync(int skip, int count) - { - using (var connection = new SqlConnection(ConnectionString)) - { - var results = await connection.QueryAsync( - $"[{Schema}].[TaxRate_Search]", - new { Skip = skip, Count = count }, - commandType: CommandType.StoredProcedure); - - return results.ToList(); - } - } - - public async Task> GetAllActiveAsync() - { - using (var connection = new SqlConnection(ConnectionString)) - { - var results = await connection.QueryAsync( - $"[{Schema}].[TaxRate_ReadAllActive]", - commandType: CommandType.StoredProcedure); - - return results.ToList(); - } - } - - public async Task ArchiveAsync(TaxRate model) - { - using (var connection = new SqlConnection(ConnectionString)) - { - var results = await connection.ExecuteAsync( - $"[{Schema}].[TaxRate_Archive]", - new { Id = model.Id }, - commandType: CommandType.StoredProcedure); - } - } - - public async Task> GetByLocationAsync(TaxRate model) - { - using (var connection = new SqlConnection(ConnectionString)) - { - var results = await connection.QueryAsync( - $"[{Schema}].[TaxRate_ReadByLocation]", - new { Country = model.Country, PostalCode = model.PostalCode }, - commandType: CommandType.StoredProcedure); - - return results.ToList(); - } - } -} diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index f3b96c201b..3f805bbe2c 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -88,7 +88,6 @@ public static class EntityFrameworkServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Infrastructure.EntityFramework/Repositories/TaxRateRepository.cs b/src/Infrastructure.EntityFramework/Repositories/TaxRateRepository.cs deleted file mode 100644 index 38fcaaa1aa..0000000000 --- a/src/Infrastructure.EntityFramework/Repositories/TaxRateRepository.cs +++ /dev/null @@ -1,68 +0,0 @@ -using AutoMapper; -using Bit.Core.Repositories; -using Bit.Infrastructure.EntityFramework.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; - -#nullable enable - -namespace Bit.Infrastructure.EntityFramework.Repositories; - -public class TaxRateRepository : Repository, ITaxRateRepository -{ - public TaxRateRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) - : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.TaxRates) - { } - - public async Task ArchiveAsync(Core.Entities.TaxRate model) - { - using (var scope = ServiceScopeFactory.CreateScope()) - { - var dbContext = GetDatabaseContext(scope); - await dbContext.TaxRates - .Where(tr => tr.Id == model.Id) - .ExecuteUpdateAsync(property => property.SetProperty(tr => tr.Active, false)); - } - } - - public async Task> GetAllActiveAsync() - { - using (var scope = ServiceScopeFactory.CreateScope()) - { - var dbContext = GetDatabaseContext(scope); - var results = await dbContext.TaxRates - .Where(t => t.Active) - .ToListAsync(); - return Mapper.Map>(results); - } - } - - public async Task> GetByLocationAsync(Core.Entities.TaxRate taxRate) - { - using (var scope = ServiceScopeFactory.CreateScope()) - { - var dbContext = GetDatabaseContext(scope); - var results = await dbContext.TaxRates - .Where(t => t.Active && - t.Country == taxRate.Country && - t.PostalCode == taxRate.PostalCode) - .ToListAsync(); - return Mapper.Map>(results); - } - } - - public async Task> SearchAsync(int skip, int count) - { - using (var scope = ServiceScopeFactory.CreateScope()) - { - var dbContext = GetDatabaseContext(scope); - var results = await dbContext.TaxRates - .Skip(skip) - .Take(count) - .Where(t => t.Active) - .OrderBy(t => t.Country).ThenByDescending(t => t.PostalCode) - .ToListAsync(); - return Mapper.Map>(results); - } - } -} diff --git a/test/Infrastructure.EFIntegration.Test/AutoFixture/TaxRateFixtures.cs b/test/Infrastructure.EFIntegration.Test/AutoFixture/TaxRateFixtures.cs deleted file mode 100644 index c8cd8c692c..0000000000 --- a/test/Infrastructure.EFIntegration.Test/AutoFixture/TaxRateFixtures.cs +++ /dev/null @@ -1,56 +0,0 @@ -using AutoFixture; -using AutoFixture.Kernel; -using Bit.Core.Entities; -using Bit.Infrastructure.EFIntegration.Test.AutoFixture.Relays; -using Bit.Infrastructure.EntityFramework.Repositories; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; - -namespace Bit.Infrastructure.EFIntegration.Test.AutoFixture; - -internal class TaxRateBuilder : ISpecimenBuilder -{ - public object Create(object request, ISpecimenContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - var type = request as Type; - if (type == null || type != typeof(TaxRate)) - { - return new NoSpecimen(); - } - - var fixture = new Fixture(); - fixture.Customizations.Insert(0, new MaxLengthStringRelay()); - var obj = fixture.WithAutoNSubstitutions().Create(); - return obj; - } -} - -internal class EfTaxRate : ICustomization -{ - public void Customize(IFixture fixture) - { - fixture.Customizations.Add(new IgnoreVirtualMembersCustomization()); - fixture.Customizations.Add(new GlobalSettingsBuilder()); - fixture.Customizations.Add(new TaxRateBuilder()); - fixture.Customizations.Add(new EfRepositoryListBuilder()); - } -} - -internal class EfTaxRateAutoDataAttribute : CustomAutoDataAttribute -{ - public EfTaxRateAutoDataAttribute() : base(new SutProviderCustomization(), new EfTaxRate()) - { } -} - -internal class InlineEfTaxRateAutoDataAttribute : InlineCustomAutoDataAttribute -{ - public InlineEfTaxRateAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization), - typeof(EfTaxRate) }, values) - { } -} - diff --git a/test/Infrastructure.EFIntegration.Test/Repositories/TaxRateRepositoryTests.cs b/test/Infrastructure.EFIntegration.Test/Repositories/TaxRateRepositoryTests.cs deleted file mode 100644 index e2c6d03b44..0000000000 --- a/test/Infrastructure.EFIntegration.Test/Repositories/TaxRateRepositoryTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Bit.Core.Entities; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Infrastructure.EFIntegration.Test.AutoFixture; -using Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers; -using Xunit; -using EfRepo = Bit.Infrastructure.EntityFramework.Repositories; -using SqlRepo = Bit.Infrastructure.Dapper.Repositories; - -namespace Bit.Infrastructure.EFIntegration.Test.Repositories; - -public class TaxRateRepositoryTests -{ - [CiSkippedTheory, EfTaxRateAutoData] - public async Task CreateAsync_Works_DataMatches( - TaxRate taxRate, - TaxRateCompare equalityComparer, - List suts, - SqlRepo.TaxRateRepository sqlTaxRateRepo - ) - { - var savedTaxRates = new List(); - foreach (var sut in suts) - { - var i = suts.IndexOf(sut); - var postEfTaxRate = await sut.CreateAsync(taxRate); - sut.ClearChangeTracking(); - - var savedTaxRate = await sut.GetByIdAsync(postEfTaxRate.Id); - savedTaxRates.Add(savedTaxRate); - } - - var sqlTaxRate = await sqlTaxRateRepo.CreateAsync(taxRate); - var savedSqlTaxRate = await sqlTaxRateRepo.GetByIdAsync(sqlTaxRate.Id); - savedTaxRates.Add(savedSqlTaxRate); - - var distinctItems = savedTaxRates.Distinct(equalityComparer); - Assert.True(!distinctItems.Skip(1).Any()); - } -} From 904692a9b61debbb3d6b9d1f7041aa00d550440e Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Fri, 10 Jan 2025 13:43:58 -0500 Subject: [PATCH 004/148] [pm-10860] Fix provider name encoding issue. (#5244) Prevent double encoding, as Handlebars encode strings by default. --- src/Core/Services/Implementations/HandlebarsMailService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index deae80c056..556483d8f4 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -863,7 +863,7 @@ public class HandlebarsMailService : IMailService var message = CreateDefaultMessage($"Join {providerName}", email); var model = new ProviderUserInvitedViewModel { - ProviderName = CoreHelpers.SanitizeForEmail(providerName), + ProviderName = CoreHelpers.SanitizeForEmail(providerName, false), Email = WebUtility.UrlEncode(providerUser.Email), ProviderId = providerUser.ProviderId.ToString(), ProviderUserId = providerUser.Id.ToString(), From 730f83b4259d150665261ce9d810c891b94ad071 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Fri, 10 Jan 2025 14:19:52 -0600 Subject: [PATCH 005/148] Fixing misspelling. made changes to domain claim email. (#5248) --- .../TwoFactorAuthenticationPolicyValidator.cs | 2 +- .../AdminConsole/DomainClaimedByOrganization.html.hbs | 6 +----- src/Core/Services/IMailService.cs | 2 +- src/Core/Services/Implementations/HandlebarsMailService.cs | 4 ++-- src/Core/Services/Implementations/UserService.cs | 2 +- src/Core/Services/NoopImplementations/NoopMailService.cs | 2 +- .../TwoFactorAuthenticationPolicyValidatorTests.cs | 2 +- test/Core.Test/Services/UserServiceTests.cs | 6 +++--- 8 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs index 6c217cecbe..04984b17be 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs @@ -111,7 +111,7 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator } await Task.WhenAll(currentActiveRevocableOrganizationUsers.Select(x => - _mailService.SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(organization.DisplayName(), x.Email))); + _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), x.Email))); } private async Task RemoveNonCompliantUsersAsync(Guid organizationId) diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.html.hbs index ad2245e585..f10c47c78f 100644 --- a/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.html.hbs +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.html.hbs @@ -1,14 +1,10 @@ {{#>TitleContactUsHtmlLayout}} - - -
- As a member of {{OrganizationName}}, your Bitwarden account is claimed and owned by your organization. -
Here's what that means:
    +
  • Your Bitwarden account is owned by {{OrganizationName}}
  • Your administrators can delete your account at any time
  • You cannot leave the organization
diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 0f69d8daaf..09bae38a03 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -36,7 +36,7 @@ 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 SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email); Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email); Task SendPasswordlessSignInAsync(string returnUrl, string token, string email); Task SendInvoiceUpcoming( diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 556483d8f4..c4ca48d3a3 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -295,7 +295,7 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(string organizationName, string email) + public async Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email) { var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email); var model = new OrganizationUserRevokedForPolicyTwoFactorViewModel @@ -472,7 +472,7 @@ public class HandlebarsMailService : IMailService "AdminConsole.DomainClaimedByOrganization", new ClaimedDomainUserNotificationViewModel { - TitleFirst = $"Hey {emailAddress}, your account is owned by {org.DisplayName()}", + TitleFirst = $"Your Bitwarden account is claimed by {org.DisplayName()}", OrganizationName = CoreHelpers.SanitizeForEmail(org.DisplayName(), false) }); } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 78da7b42e3..1dd8c3f8ca 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1383,7 +1383,7 @@ public class UserService : UserManager, IUserService, IDisposable p.OrganizationId, [new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }], new SystemUser(EventSystemUser.TwoFactorDisabled))); - await _mailService.SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(organization.DisplayName(), user.Email); + await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email); } else { diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 4ce188c86b..0e07436fd1 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -80,7 +80,7 @@ public class NoopMailService : IMailService return Task.FromResult(0); } - public Task SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(string organizationName, string email) => + public Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email) => Task.CompletedTask; public Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email) => diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs index 8c350d4161..0edc2b5973 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs @@ -351,7 +351,7 @@ public class TwoFactorAuthenticationPolicyValidatorTests await sutProvider.GetDependency() .Received(1) - .SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(organization.DisplayName(), + .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), "user3@test.com"); } } diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index d8a0ade1fa..9539767f6f 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -517,7 +517,7 @@ public class UserServiceTests r.OrganizationUsers.First().OrganizationId == organization1.Id)); await sutProvider.GetDependency() .Received(1) - .SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(organization1.DisplayName(), user.Email); + .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization1.DisplayName(), user.Email); // Remove the user from the second organization await sutProvider.GetDependency() @@ -528,7 +528,7 @@ public class UserServiceTests r.OrganizationUsers.First().OrganizationId == organization2.Id)); await sutProvider.GetDependency() .Received(1) - .SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(organization2.DisplayName(), user.Email); + .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization2.DisplayName(), user.Email); } [Theory, BitAutoData] @@ -572,7 +572,7 @@ public class UserServiceTests .RevokeNonCompliantOrganizationUsersAsync(default); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(default, default); + .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default); } [Theory, BitAutoData] From aa0b35a345618e10f41e41bcb431d16043179d80 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 10 Jan 2025 15:54:53 -0500 Subject: [PATCH 006/148] [PM-15608] Create more KDF defaults for prelogin (#5122) * kdf defaults on null map to email hash * cleanup code. add some randomness as well * remove null check * fix test * move to private method * remove random options * tests for random defaults * SetDefaultKdfHmacKey for old test --- src/Core/Settings/GlobalSettings.cs | 1 + .../Controllers/AccountsController.cs | 73 +++++++++++++++++-- .../Controllers/AccountsControllerTests.cs | 67 ++++++++++++++++- 3 files changed, 132 insertions(+), 9 deletions(-) diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 420151a34f..97d66aed53 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -82,6 +82,7 @@ public class GlobalSettings : IGlobalSettings public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings(); public virtual string DevelopmentDirectory { get; set; } public virtual bool EnableEmailVerification { get; set; } + public virtual string KdfDefaultHashKey { get; set; } public virtual string PricingUri { get; set; } public string BuildExternalUri(string explicitValue, string name) diff --git a/src/Identity/Controllers/AccountsController.cs b/src/Identity/Controllers/AccountsController.cs index 40c926bda0..c1ecff9620 100644 --- a/src/Identity/Controllers/AccountsController.cs +++ b/src/Identity/Controllers/AccountsController.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Text; using Bit.Core; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Api.Request.Accounts; @@ -15,6 +16,7 @@ using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Settings; using Bit.Core.Tokens; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; @@ -44,6 +46,41 @@ public class AccountsController : Controller private readonly IFeatureService _featureService; private readonly IDataProtectorTokenFactory _registrationEmailVerificationTokenDataFactory; + private readonly byte[] _defaultKdfHmacKey = null; + private static readonly List _defaultKdfResults = + [ + // The first result (index 0) should always return the "normal" default. + new() + { + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default, + }, + // We want more weight for this default, so add it again + new() + { + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default, + }, + // Add some other possible defaults... + new() + { + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 100_000, + }, + new() + { + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 5_000, + }, + new() + { + Kdf = KdfType.Argon2id, + KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default, + KdfMemory = AuthConstants.ARGON2_MEMORY.Default, + KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default, + } + ]; + public AccountsController( ICurrentContext currentContext, ILogger logger, @@ -55,7 +92,8 @@ public class AccountsController : Controller ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand, IReferenceEventService referenceEventService, IFeatureService featureService, - IDataProtectorTokenFactory registrationEmailVerificationTokenDataFactory + IDataProtectorTokenFactory registrationEmailVerificationTokenDataFactory, + GlobalSettings globalSettings ) { _currentContext = currentContext; @@ -69,6 +107,11 @@ public class AccountsController : Controller _referenceEventService = referenceEventService; _featureService = featureService; _registrationEmailVerificationTokenDataFactory = registrationEmailVerificationTokenDataFactory; + + if (CoreHelpers.SettingHasValue(globalSettings.KdfDefaultHashKey)) + { + _defaultKdfHmacKey = Encoding.UTF8.GetBytes(globalSettings.KdfDefaultHashKey); + } } [HttpPost("register")] @@ -217,11 +260,7 @@ public class AccountsController : Controller var kdfInformation = await _userRepository.GetKdfInformationByEmailAsync(model.Email); if (kdfInformation == null) { - kdfInformation = new UserKdfInformation - { - Kdf = KdfType.PBKDF2_SHA256, - KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default, - }; + kdfInformation = GetDefaultKdf(model.Email); } return new PreloginResponseModel(kdfInformation); } @@ -240,4 +279,26 @@ public class AccountsController : Controller Token = token }; } + + private UserKdfInformation GetDefaultKdf(string email) + { + if (_defaultKdfHmacKey == null) + { + return _defaultKdfResults[0]; + } + else + { + // Compute the HMAC hash of the email + var hmacMessage = Encoding.UTF8.GetBytes(email.Trim().ToLowerInvariant()); + using var hmac = new System.Security.Cryptography.HMACSHA256(_defaultKdfHmacKey); + var hmacHash = hmac.ComputeHash(hmacMessage); + // Convert the hash to a number + var hashHex = BitConverter.ToString(hmacHash).Replace("-", string.Empty).ToLowerInvariant(); + var hashFirst8Bytes = hashHex.Substring(0, 16); + var hashNumber = long.Parse(hashFirst8Bytes, System.Globalization.NumberStyles.HexNumber); + // Find the default KDF value for this hash number + var hashIndex = (int)(Math.Abs(hashNumber) % _defaultKdfResults.Count); + return _defaultKdfResults[hashIndex]; + } + } } diff --git a/test/Identity.Test/Controllers/AccountsControllerTests.cs b/test/Identity.Test/Controllers/AccountsControllerTests.cs index 8acebbabe0..03db0a5904 100644 --- a/test/Identity.Test/Controllers/AccountsControllerTests.cs +++ b/test/Identity.Test/Controllers/AccountsControllerTests.cs @@ -1,4 +1,6 @@ -using Bit.Core; +using System.Reflection; +using System.Text; +using Bit.Core; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Services; @@ -11,6 +13,7 @@ using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Settings; using Bit.Core.Tokens; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; @@ -42,6 +45,7 @@ public class AccountsControllerTests : IDisposable private readonly IReferenceEventService _referenceEventService; private readonly IFeatureService _featureService; private readonly IDataProtectorTokenFactory _registrationEmailVerificationTokenDataFactory; + private readonly GlobalSettings _globalSettings; public AccountsControllerTests() @@ -57,6 +61,7 @@ public class AccountsControllerTests : IDisposable _referenceEventService = Substitute.For(); _featureService = Substitute.For(); _registrationEmailVerificationTokenDataFactory = Substitute.For>(); + _globalSettings = Substitute.For(); _sut = new AccountsController( _currentContext, @@ -69,7 +74,8 @@ public class AccountsControllerTests : IDisposable _sendVerificationEmailForRegistrationCommand, _referenceEventService, _featureService, - _registrationEmailVerificationTokenDataFactory + _registrationEmailVerificationTokenDataFactory, + _globalSettings ); } @@ -95,8 +101,9 @@ public class AccountsControllerTests : IDisposable } [Fact] - public async Task PostPrelogin_WhenUserDoesNotExist_ShouldDefaultToPBKDF() + public async Task PostPrelogin_WhenUserDoesNotExistAndNoDefaultKdfHmacKeySet_ShouldDefaultToPBKDF() { + SetDefaultKdfHmacKey(null); _userRepository.GetKdfInformationByEmailAsync(Arg.Any()).Returns(Task.FromResult(null)); var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = "user@example.com" }); @@ -105,6 +112,38 @@ public class AccountsControllerTests : IDisposable Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, response.KdfIterations); } + [Theory] + [BitAutoData] + public async Task PostPrelogin_WhenUserDoesNotExistAndDefaultKdfHmacKeyIsSet_ShouldComputeHmacAndReturnExpectedKdf(string email) + { + // Arrange: + var defaultKey = Encoding.UTF8.GetBytes("my-secret-key"); + SetDefaultKdfHmacKey(defaultKey); + + _userRepository.GetKdfInformationByEmailAsync(Arg.Any()).Returns(Task.FromResult(null)); + + var fieldInfo = typeof(AccountsController).GetField("_defaultKdfResults", BindingFlags.NonPublic | BindingFlags.Static); + if (fieldInfo == null) + throw new InvalidOperationException("Field '_defaultKdfResults' not found."); + + var defaultKdfResults = (List)fieldInfo.GetValue(null)!; + + var expectedIndex = GetExpectedKdfIndex(email, defaultKey, defaultKdfResults); + var expectedKdf = defaultKdfResults[expectedIndex]; + + // Act + var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = email }); + + // Assert: Ensure the returned KDF matches the expected one from the computed hash + Assert.Equal(expectedKdf.Kdf, response.Kdf); + Assert.Equal(expectedKdf.KdfIterations, response.KdfIterations); + if (expectedKdf.Kdf == KdfType.Argon2id) + { + Assert.Equal(expectedKdf.KdfMemory, response.KdfMemory); + Assert.Equal(expectedKdf.KdfParallelism, response.KdfParallelism); + } + } + [Fact] public async Task PostRegister_ShouldRegisterUser() { @@ -484,6 +523,28 @@ public class AccountsControllerTests : IDisposable )); } + private void SetDefaultKdfHmacKey(byte[]? newKey) + { + var fieldInfo = typeof(AccountsController).GetField("_defaultKdfHmacKey", BindingFlags.NonPublic | BindingFlags.Instance); + if (fieldInfo == null) + { + throw new InvalidOperationException("Field '_defaultKdfHmacKey' not found."); + } + fieldInfo.SetValue(_sut, newKey); + } + private int GetExpectedKdfIndex(string email, byte[] defaultKey, List defaultKdfResults) + { + // Compute the HMAC hash of the email + var hmacMessage = Encoding.UTF8.GetBytes(email.Trim().ToLowerInvariant()); + using var hmac = new System.Security.Cryptography.HMACSHA256(defaultKey); + var hmacHash = hmac.ComputeHash(hmacMessage); + + // Convert the hash to a number and calculate the index + var hashHex = BitConverter.ToString(hmacHash).Replace("-", string.Empty).ToLowerInvariant(); + var hashFirst8Bytes = hashHex.Substring(0, 16); + var hashNumber = long.Parse(hashFirst8Bytes, System.Globalization.NumberStyles.HexNumber); + return (int)(Math.Abs(hashNumber) % defaultKdfResults.Count); + } } From 72bb06a9d766a758c530cbd276cc96c71d759b23 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Fri, 10 Jan 2025 21:55:34 -0500 Subject: [PATCH 007/148] Auth/PM-16947 - Device Management - Adjust Device + pending auth request get query (#5250) * Added userId check on query * Added required field to inner select * PM-16947 - Update to filter inner subquery on user id per discussion with Robert * Updated to use new query with ROW_NUMBER * More query optimizations to eliminate returning old requests for a device * Fixed approval condition to be NULL as 0 means denied. * Added negation of @ExpirationMinutes --------- Co-authored-by: Todd Martin --- ...dActiveWithPendingAuthRequestsByUserId.sql | 25 +++++++++------- ...dActiveWithPendingAuthRequestsByUserId.sql | 29 +++++++++++++++++++ 2 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-01-10_00_ReadActiveWithPendingAuthRequestsByUserId.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/Device_ReadActiveWithPendingAuthRequestsByUserId.sql b/src/Sql/Auth/dbo/Stored Procedures/Device_ReadActiveWithPendingAuthRequestsByUserId.sql index 015d0f7c1f..f40e9149c0 100644 --- a/src/Sql/Auth/dbo/Stored Procedures/Device_ReadActiveWithPendingAuthRequestsByUserId.sql +++ b/src/Sql/Auth/dbo/Stored Procedures/Device_ReadActiveWithPendingAuthRequestsByUserId.sql @@ -10,18 +10,21 @@ BEGIN AR.Id as AuthRequestId, AR.CreationDate as AuthRequestCreationDate FROM dbo.DeviceView D - LEFT JOIN ( - SELECT TOP 1 -- Take only the top record sorted by auth request creation date - Id, - CreationDate, - RequestDeviceIdentifier + LEFT JOIN ( + SELECT + Id, + CreationDate, + RequestDeviceIdentifier, + Approved, + ROW_NUMBER() OVER (PARTITION BY RequestDeviceIdentifier ORDER BY CreationDate DESC) as rn FROM dbo.AuthRequestView - WHERE Type IN (0, 1) -- Include only AuthenticateAndUnlock and Unlock types, excluding Admin Approval (type 2) - AND CreationDate >= DATEADD(MINUTE, -@ExpirationMinutes, GETUTCDATE()) -- Ensure the request hasn't expired - AND Approved IS NULL -- Include only requests that haven't been acknowledged or approved - ORDER BY CreationDate DESC - ) AR ON D.Identifier = AR.RequestDeviceIdentifier + WHERE Type IN (0, 1) -- AuthenticateAndUnlock and Unlock types only + AND CreationDate >= DATEADD(MINUTE, -@ExpirationMinutes, GETUTCDATE()) -- Ensure the request hasn't expired + AND UserId = @UserId -- Requests for this user only + ) AR -- This join will get the most recent request per device, regardless of approval status + ON D.Identifier = AR.RequestDeviceIdentifier AND AR.rn = 1 AND AR.Approved IS NULL -- Get only the most recent unapproved request per device WHERE - D.UserId = @UserId + D.UserId = @UserId -- Include only devices for this user AND D.Active = 1; -- Include only active devices END; + diff --git a/util/Migrator/DbScripts/2025-01-10_00_ReadActiveWithPendingAuthRequestsByUserId.sql b/util/Migrator/DbScripts/2025-01-10_00_ReadActiveWithPendingAuthRequestsByUserId.sql new file mode 100644 index 0000000000..10319f3207 --- /dev/null +++ b/util/Migrator/DbScripts/2025-01-10_00_ReadActiveWithPendingAuthRequestsByUserId.sql @@ -0,0 +1,29 @@ +CREATE OR ALTER PROCEDURE [dbo].[Device_ReadActiveWithPendingAuthRequestsByUserId] + @UserId UNIQUEIDENTIFIER, + @ExpirationMinutes INT +AS +BEGIN + SET NOCOUNT ON; + + SELECT + D.*, + AR.Id as AuthRequestId, + AR.CreationDate as AuthRequestCreationDate + FROM dbo.DeviceView D + LEFT JOIN ( + SELECT + Id, + CreationDate, + RequestDeviceIdentifier, + Approved, + ROW_NUMBER() OVER (PARTITION BY RequestDeviceIdentifier ORDER BY CreationDate DESC) as rn + FROM dbo.AuthRequestView + WHERE Type IN (0, 1) -- AuthenticateAndUnlock and Unlock types only + AND CreationDate >= DATEADD(MINUTE, -@ExpirationMinutes, GETUTCDATE()) -- Ensure the request hasn't expired + AND UserId = @UserId -- Requests for this user only + ) AR -- This join will get the most recent request per device, regardless of approval status + ON D.Identifier = AR.RequestDeviceIdentifier AND AR.rn = 1 AND AR.Approved IS NULL -- Get only the most recent unapproved request per device + WHERE + D.UserId = @UserId -- Include only devices for this user + AND D.Active = 1; -- Include only active devices +END; From 6c7b881e5160e93a527aa3d603c85fd9af8b9f80 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 13 Jan 2025 20:04:15 +0000 Subject: [PATCH 008/148] Bumped version to 2025.1.3 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 1467c0822c..40e6fdd202 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.1.2 + 2025.1.3 Bit.$(MSBuildProjectName) enable From 82508fb7a962ee81fd3d1210b4fdc8b5f512c93f Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 13 Jan 2025 14:54:32 -0600 Subject: [PATCH 009/148] fix: remove delete from cs/billing and create new RequestDelete perm, refs PM-17014 (#5258) --- src/Admin/AdminConsole/Views/Organizations/Edit.cshtml | 6 +++++- src/Admin/Enums/Permissions.cs | 1 + src/Admin/Utilities/RolePermissionMapping.cs | 6 ++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Admin/AdminConsole/Views/Organizations/Edit.cshtml b/src/Admin/AdminConsole/Views/Organizations/Edit.cshtml index f7207fc7e8..3ac716a6d4 100644 --- a/src/Admin/AdminConsole/Views/Organizations/Edit.cshtml +++ b/src/Admin/AdminConsole/Views/Organizations/Edit.cshtml @@ -10,6 +10,7 @@ var canViewOrganizationInformation = AccessControlService.UserHasPermission(Permission.Org_OrgInformation_View); var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View); var canInitiateTrial = AccessControlService.UserHasPermission(Permission.Org_InitiateTrial); + var canRequestDelete = AccessControlService.UserHasPermission(Permission.Org_RequestDelete); var canDelete = AccessControlService.UserHasPermission(Permission.Org_Delete); var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit); } @@ -120,12 +121,15 @@ Unlink provider } - @if (canDelete) + @if (canRequestDelete) {
+ } + @if (canDelete) + {
diff --git a/src/Admin/Enums/Permissions.cs b/src/Admin/Enums/Permissions.cs index c878267f89..c544cb2106 100644 --- a/src/Admin/Enums/Permissions.cs +++ b/src/Admin/Enums/Permissions.cs @@ -24,6 +24,7 @@ public enum Permission Org_CheckEnabledBox, Org_BusinessInformation_View, Org_InitiateTrial, + Org_RequestDelete, Org_Delete, Org_BillingInformation_View, Org_BillingInformation_DownloadInvoice, diff --git a/src/Admin/Utilities/RolePermissionMapping.cs b/src/Admin/Utilities/RolePermissionMapping.cs index 9cee571aba..381cf914aa 100644 --- a/src/Admin/Utilities/RolePermissionMapping.cs +++ b/src/Admin/Utilities/RolePermissionMapping.cs @@ -30,6 +30,7 @@ public static class RolePermissionMapping Permission.Org_BusinessInformation_View, Permission.Org_InitiateTrial, Permission.Org_Delete, + Permission.Org_RequestDelete, Permission.Org_BillingInformation_View, Permission.Org_BillingInformation_DownloadInvoice, Permission.Org_Plan_View, @@ -74,6 +75,7 @@ public static class RolePermissionMapping Permission.Org_GeneralDetails_View, Permission.Org_BusinessInformation_View, Permission.Org_Delete, + Permission.Org_RequestDelete, Permission.Org_BillingInformation_View, Permission.Org_BillingInformation_DownloadInvoice, Permission.Org_BillingInformation_CreateEditTransaction, @@ -114,7 +116,6 @@ 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, @@ -124,6 +125,7 @@ public static class RolePermissionMapping Permission.Org_Licensing_View, Permission.Org_Billing_View, Permission.Org_Billing_LaunchGateway, + Permission.Org_RequestDelete, Permission.Provider_List_View, Permission.Provider_View } @@ -157,7 +159,7 @@ public static class RolePermissionMapping Permission.Org_Billing_View, Permission.Org_Billing_Edit, Permission.Org_Billing_LaunchGateway, - Permission.Org_Delete, + Permission.Org_RequestDelete, Permission.Provider_Edit, Permission.Provider_View, Permission.Provider_List_View, From 0645f51b65f972617f24f701bae3f856c2196943 Mon Sep 17 00:00:00 2001 From: Matt Andreko Date: Mon, 13 Jan 2025 17:02:35 -0500 Subject: [PATCH 010/148] Removed unnecessary github token (#5259) --- .github/workflows/scan.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index e40ff07148..156ebee165 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -80,7 +80,6 @@ jobs: - name: Scan with SonarCloud env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | dotnet-sonarscanner begin /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" \ /d:sonar.test.inclusions=test/,bitwarden_license/test/ \ From 1c73a997d9831d9dc0bd792345d050ed1acc3a8d Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Tue, 14 Jan 2025 13:36:28 -0500 Subject: [PATCH 011/148] [14026] Update endpoint document model type (#5191) --- src/Api/AdminConsole/Public/Controllers/PoliciesController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs index a22c05ed62..d261a3c555 100644 --- a/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs @@ -41,7 +41,7 @@ public class PoliciesController : Controller /// /// The type of policy to be retrieved. [HttpGet("{type}")] - [ProducesResponseType(typeof(GroupResponseModel), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(PolicyResponseModel), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task Get(PolicyType type) { From 79810b78fff8eb2047a64740fc432177848e5041 Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Tue, 14 Jan 2025 13:38:32 -0500 Subject: [PATCH 012/148] [pm-14415] Add recommended extensions for VS Code. (#5249) --- .vscode/extensions.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..3282b1c509 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,18 @@ +{ + "recommendations": [ + "nick-rudenko.back-n-forth", + "streetsidesoftware.code-spell-checker", + "MS-vsliveshare.vsliveshare", + + "mhutchie.git-graph", + "donjayamanne.githistory", + "eamodio.gitlens", + + "jakebathman.mysql-syntax", + "ckolkman.vscode-postgres", + + "ms-dotnettools.csharp", + "formulahendry.dotnet-test-explorer", + "adrianwilczynski.user-secrets" + ] +} From 95893bd0b1eddd60bd3639314ce5259d68e2dc1f Mon Sep 17 00:00:00 2001 From: Graham Walker Date: Tue, 14 Jan 2025 13:16:59 -0600 Subject: [PATCH 013/148] PM-16170 removing deprecated send file endpoint (#5222) --- src/Api/Tools/Controllers/SendsController.cs | 27 -------------------- 1 file changed, 27 deletions(-) diff --git a/src/Api/Tools/Controllers/SendsController.cs b/src/Api/Tools/Controllers/SendsController.cs index f7f3f692a8..3b5534bed0 100644 --- a/src/Api/Tools/Controllers/SendsController.cs +++ b/src/Api/Tools/Controllers/SendsController.cs @@ -9,7 +9,6 @@ using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; @@ -163,32 +162,6 @@ public class SendsController : Controller return new SendResponseModel(send, _globalSettings); } - [HttpPost("file")] - [Obsolete("Deprecated File Send API", false)] - [RequestSizeLimit(Constants.FileSize101mb)] - [DisableFormValueModelBinding] - public async Task PostFile() - { - if (!Request?.ContentType.Contains("multipart/") ?? true) - { - throw new BadRequestException("Invalid content."); - } - - Send send = null; - await Request.GetSendFileAsync(async (stream, fileName, model) => - { - model.ValidateCreation(); - var userId = _userService.GetProperUserId(User).Value; - var (madeSend, madeData) = model.ToSend(userId, fileName, _sendService); - send = madeSend; - await _sendService.SaveFileSendAsync(send, madeData, model.FileLength.GetValueOrDefault(0)); - await _sendService.UploadFileToExistingSendAsync(stream, send); - }); - - return new SendResponseModel(send, _globalSettings); - } - - [HttpPost("file/v2")] public async Task PostFile([FromBody] SendRequestModel model) { From becc6b2da14d4ed77298892fea7df65dc89a00fb Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Tue, 14 Jan 2025 15:47:35 -0500 Subject: [PATCH 014/148] add NotificationRefresh feature flag (#5262) Co-authored-by: Evan Bassler --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index f06f63573d..62ce964001 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -133,6 +133,7 @@ public static class FeatureFlagKeys public const string AccountDeprovisioning = "pm-10308-account-deprovisioning"; public const string NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements"; public const string BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain"; + public const string NotificationRefresh = "notification-refresh"; public const string AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api"; public const string PersistPopupView = "persist-popup-view"; public const string CipherKeyEncryption = "cipher-key-encryption"; From 34ce4805685291ffd51a6301737225e67df82caa Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Wed, 15 Jan 2025 09:31:59 -0500 Subject: [PATCH 015/148] fix(email-feature-flags): [PM-7882] Email Verification - Removed the email feature flag from server. (#5232) --- src/Identity/Billing/Controller/AccountsController.cs | 5 +---- src/Identity/Controllers/AccountsController.cs | 4 ---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Identity/Billing/Controller/AccountsController.cs b/src/Identity/Billing/Controller/AccountsController.cs index f06fc7bf2c..aada40bcb2 100644 --- a/src/Identity/Billing/Controller/AccountsController.cs +++ b/src/Identity/Billing/Controller/AccountsController.cs @@ -1,11 +1,9 @@ -using Bit.Core; -using Bit.Core.Billing.Models.Api.Requests.Accounts; +using Bit.Core.Billing.Models.Api.Requests.Accounts; using Bit.Core.Billing.TrialInitiation.Registration; using Bit.Core.Context; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; -using Bit.Core.Utilities; using Bit.SharedWeb.Utilities; using Microsoft.AspNetCore.Mvc; @@ -18,7 +16,6 @@ public class AccountsController( ISendTrialInitiationEmailForRegistrationCommand sendTrialInitiationEmailForRegistrationCommand, IReferenceEventService referenceEventService) : Microsoft.AspNetCore.Mvc.Controller { - [RequireFeature(FeatureFlagKeys.EmailVerification)] [HttpPost("trial/send-verification-email")] public async Task PostTrialInitiationSendVerificationEmailAsync([FromBody] TrialSendVerificationEmailRequestModel model) { diff --git a/src/Identity/Controllers/AccountsController.cs b/src/Identity/Controllers/AccountsController.cs index c1ecff9620..f2f68dbec1 100644 --- a/src/Identity/Controllers/AccountsController.cs +++ b/src/Identity/Controllers/AccountsController.cs @@ -21,7 +21,6 @@ using Bit.Core.Tokens; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; -using Bit.Core.Utilities; using Bit.Identity.Models.Request.Accounts; using Bit.Identity.Models.Response.Accounts; using Bit.SharedWeb.Utilities; @@ -125,7 +124,6 @@ public class AccountsController : Controller return await ProcessRegistrationResult(identityResult, user, delaysEnabled: true); } - [RequireFeature(FeatureFlagKeys.EmailVerification)] [HttpPost("register/send-verification-email")] public async Task PostRegisterSendVerificationEmail([FromBody] RegisterSendVerificationEmailRequestModel model) { @@ -149,7 +147,6 @@ public class AccountsController : Controller return NoContent(); } - [RequireFeature(FeatureFlagKeys.EmailVerification)] [HttpPost("register/verification-email-clicked")] public async Task PostRegisterVerificationEmailClicked([FromBody] RegisterVerificationEmailClickedRequestModel model) { @@ -182,7 +179,6 @@ public class AccountsController : Controller } - [RequireFeature(FeatureFlagKeys.EmailVerification)] [HttpPost("register/finish")] public async Task PostRegisterFinish([FromBody] RegisterFinishRequestModel model) { From a3e3c7f96c09266584d7576253f9c7e7ece0acdb Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 15 Jan 2025 09:45:13 -0500 Subject: [PATCH 016/148] fix: Added web browser clients to allowed approving device types --- src/Identity/Utilities/LoginApprovingDeviceTypes.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Identity/Utilities/LoginApprovingDeviceTypes.cs b/src/Identity/Utilities/LoginApprovingDeviceTypes.cs index 46b4606ccf..b8b11a4d19 100644 --- a/src/Identity/Utilities/LoginApprovingDeviceTypes.cs +++ b/src/Identity/Utilities/LoginApprovingDeviceTypes.cs @@ -12,6 +12,7 @@ public static class LoginApprovingDeviceTypes var deviceTypes = new List(); deviceTypes.AddRange(DeviceTypes.DesktopTypes); deviceTypes.AddRange(DeviceTypes.MobileTypes); + deviceTypes.AddRange(DeviceTypes.BrowserTypes); _deviceTypes = deviceTypes.AsReadOnly(); } From cc2128c97a74030c530e8f72d929e109ea473df8 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Wed, 15 Jan 2025 16:05:27 +0100 Subject: [PATCH 017/148] =?UTF-8?q?[PM-16979]=20Avoid=20returning=20Billin?= =?UTF-8?q?gTaxIdTypeInterferenceError=20when=20an=20=E2=80=A6=20(#5252)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [PM-16979] Avoid returning BillingTaxIdTypeInterferenceError when an empty tax id string is passed * tests * fix tests --- .../Implementations/StripePaymentService.cs | 4 +- .../Services/StripePaymentServiceTests.cs | 71 +++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index bf39085ea9..98d3549c14 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -119,7 +119,7 @@ public class StripePaymentService : IPaymentService Subscription subscription; try { - if (taxInfo.TaxIdNumber != null && taxInfo.TaxIdType == null) + if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber)) { taxInfo.TaxIdType = _taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry, taxInfo.TaxIdNumber); @@ -2058,7 +2058,7 @@ public class StripePaymentService : IPaymentService } } - if (!string.IsNullOrEmpty(parameters.TaxInformation.TaxId)) + if (!string.IsNullOrWhiteSpace(parameters.TaxInformation.TaxId)) { var taxIdType = _taxService.GetStripeTaxCode( options.CustomerDetails.Address.Country, diff --git a/test/Core.Test/Services/StripePaymentServiceTests.cs b/test/Core.Test/Services/StripePaymentServiceTests.cs index 35e1901a2f..11a19656e1 100644 --- a/test/Core.Test/Services/StripePaymentServiceTests.cs +++ b/test/Core.Test/Services/StripePaymentServiceTests.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; @@ -39,6 +40,11 @@ public class StripePaymentServiceTests { var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); + sutProvider + .GetDependency() + .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + var stripeAdapter = sutProvider.GetDependency(); stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer { @@ -95,6 +101,12 @@ public class StripePaymentServiceTests { var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); organization.UseSecretsManager = true; + + sutProvider + .GetDependency() + .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + var stripeAdapter = sutProvider.GetDependency(); stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer { @@ -152,6 +164,12 @@ public class StripePaymentServiceTests { var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); organization.UseSecretsManager = true; + + sutProvider + .GetDependency() + .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + var stripeAdapter = sutProvider.GetDependency(); stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer { @@ -210,6 +228,11 @@ public class StripePaymentServiceTests var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); paymentToken = "pm_" + paymentToken; + sutProvider + .GetDependency() + .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + var stripeAdapter = sutProvider.GetDependency(); stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer { @@ -397,6 +420,11 @@ public class StripePaymentServiceTests { var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); + sutProvider + .GetDependency() + .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + var stripeAdapter = sutProvider.GetDependency(); stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer { @@ -462,6 +490,12 @@ public class StripePaymentServiceTests { var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); organization.UseSecretsManager = true; + + sutProvider + .GetDependency() + .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + var stripeAdapter = sutProvider.GetDependency(); stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer { @@ -610,6 +644,43 @@ public class StripePaymentServiceTests await braintreeGateway.Customer.Received(1).DeleteAsync("Braintree-Id"); } + [Theory] + [BitAutoData("ES", "A5372895732985327895237")] + public async Task PurchaseOrganizationAsync_ThrowsBadRequestException_WhenTaxIdInvalid(string country, string taxId, SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) + { + taxInfo.BillingAddressCountry = country; + taxInfo.TaxIdNumber = taxId; + taxInfo.TaxIdType = null; + + var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); + organization.UseSecretsManager = true; + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer + { + Id = "C-1", + }); + stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription + { + Id = "S-1", + CurrentPeriodEnd = DateTime.Today.AddDays(10), + }); + sutProvider.GetDependency() + .BaseServiceUri.CloudRegion + .Returns("US"); + sutProvider + .GetDependency() + .GetStripeTaxCode(Arg.Is(p => p == country), Arg.Is(p => p == taxId)) + .Returns((string)null); + + var actual = await Assert.ThrowsAsync(async () => await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo, false, 8, 10)); + + Assert.Equal("billingTaxIdTypeInferenceError", actual.Message); + + await stripeAdapter.Received(0).CustomerCreateAsync(Arg.Any()); + await stripeAdapter.Received(0).SubscriptionCreateAsync(Arg.Any()); + } + + [Theory, BitAutoData] public async Task UpgradeFreeOrganizationAsync_Success(SutProvider sutProvider, Organization organization, TaxInfo taxInfo) From adab8e622a7b5a0cca583d54092854563691c2e5 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Wed, 15 Jan 2025 16:05:38 +0100 Subject: [PATCH 018/148] [PM-17064] 500 error on Free org Upgrade with Saved Payment Method (#5266) --- src/Core/Services/Implementations/StripePaymentService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 98d3549c14..510a1b7c3a 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -2087,12 +2087,12 @@ public class StripePaymentService : IPaymentService if (gatewayCustomer.Discount != null) { - options.Discounts.Add(new InvoiceDiscountOptions - { - Discount = gatewayCustomer.Discount.Id - }); + options.Discounts.Add(new InvoiceDiscountOptions { Discount = gatewayCustomer.Discount.Id }); } + } + if (gatewaySubscriptionId != null) + { var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId); if (gatewaySubscription?.Discount != null) From ed14f28644e9a6c847f5792e1571294a8569b13d Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Wed, 15 Jan 2025 11:04:51 -0500 Subject: [PATCH 019/148] fix(email-feature-flags): [PM-7882] Email Verification - Added back in needed import. (#5268) --- src/Identity/Controllers/AccountsController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Identity/Controllers/AccountsController.cs b/src/Identity/Controllers/AccountsController.cs index f2f68dbec1..c840a7ddc5 100644 --- a/src/Identity/Controllers/AccountsController.cs +++ b/src/Identity/Controllers/AccountsController.cs @@ -21,6 +21,7 @@ using Bit.Core.Tokens; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; +using Bit.Core.Utilities; using Bit.Identity.Models.Request.Accounts; using Bit.Identity.Models.Response.Accounts; using Bit.SharedWeb.Utilities; From 04402c1316328c1c0b72a344a95dec245cb2b00e Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:35:07 -0500 Subject: [PATCH 020/148] Updated null checks to also check for empty string or whitespace (#5272) --- src/Core/Services/Implementations/StripePaymentService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 510a1b7c3a..e14467f943 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -2081,7 +2081,7 @@ public class StripePaymentService : IPaymentService ]; } - if (gatewayCustomerId != null) + if (!string.IsNullOrWhiteSpace(gatewayCustomerId)) { var gatewayCustomer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId); @@ -2091,7 +2091,7 @@ public class StripePaymentService : IPaymentService } } - if (gatewaySubscriptionId != null) + if (!string.IsNullOrWhiteSpace(gatewaySubscriptionId)) { var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId); From d231070cac463f8dbebf434f8620175801b64b6f Mon Sep 17 00:00:00 2001 From: Matt Andreko Date: Wed, 15 Jan 2025 14:16:18 -0500 Subject: [PATCH 021/148] Removed unnecessary CODECOV_TOKEN with updated codecov-action (#5271) --- .github/workflows/test.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5cc31f5c2f..817547fc65 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -78,6 +78,3 @@ jobs: - name: Upload to codecov.io uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2 - if: ${{ needs.check-test-secrets.outputs.available == 'true' }} - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From b42e4a2261c63f7a192db244dd4202cde814e045 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:04:05 -0500 Subject: [PATCH 022/148] Adjust handling of GH action dependencies for CI/CD partnership (#5274) --- .github/renovate.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index 536ca306f8..affa29bea9 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -12,20 +12,20 @@ { "groupName": "dockerfile minor", "matchManagers": ["dockerfile"], - "matchUpdateTypes": ["minor", "patch"] + "matchUpdateTypes": ["minor"] }, { "groupName": "docker-compose minor", "matchManagers": ["docker-compose"], - "matchUpdateTypes": ["minor", "patch"] + "matchUpdateTypes": ["minor"] }, { - "groupName": "gh minor", + "groupName": "github-action minor", "matchManagers": ["github-actions"], - "matchUpdateTypes": ["minor", "patch"] + "matchUpdateTypes": ["minor"] }, { - "matchManagers": ["github-actions", "dockerfile", "docker-compose"], + "matchManagers": ["dockerfile", "docker-compose"], "commitMessagePrefix": "[deps] BRE:" }, { From 42c8c3b6f6c40b773bd37f21649536bf03f990ff Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 15 Jan 2025 21:52:11 -0500 Subject: [PATCH 023/148] [PM-17143] Add sso external id to member response model (#5273) --- .../Public/Models/Response/MemberResponseModel.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Api/AdminConsole/Public/Models/Response/MemberResponseModel.cs b/src/Api/AdminConsole/Public/Models/Response/MemberResponseModel.cs index 499c27cfc9..91e8788d01 100644 --- a/src/Api/AdminConsole/Public/Models/Response/MemberResponseModel.cs +++ b/src/Api/AdminConsole/Public/Models/Response/MemberResponseModel.cs @@ -50,6 +50,7 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel Status = user.Status; Collections = collections?.Select(c => new AssociationWithPermissionsResponseModel(c)); ResetPasswordEnrolled = user.ResetPasswordKey != null; + SsoExternalId = user.SsoExternalId; } /// @@ -104,4 +105,10 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel /// [Required] public bool ResetPasswordEnrolled { get; } + + /// + /// SSO external identifier for linking this member to an identity provider. + /// + /// sso_external_id_123456 + public string SsoExternalId { get; set; } } From 5201085ecbb333b785b290c2d3f7f77f73ee1940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:54:31 +0000 Subject: [PATCH 024/148] [PM-15193] Remove PromoteProviderServiceUser feature flag and checks from ToolsController and layout (#5255) --- src/Admin/Controllers/ToolsController.cs | 3 --- src/Admin/Views/Shared/_Layout.cshtml | 5 +---- src/Core/Constants.cs | 1 - 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Admin/Controllers/ToolsController.cs b/src/Admin/Controllers/ToolsController.cs index a84fb681e2..eaf3de4be5 100644 --- a/src/Admin/Controllers/ToolsController.cs +++ b/src/Admin/Controllers/ToolsController.cs @@ -3,7 +3,6 @@ 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; @@ -222,7 +221,6 @@ public class ToolsController : Controller return RedirectToAction("Edit", "Organizations", new { id = model.OrganizationId.Value }); } - [RequireFeature(FeatureFlagKeys.PromoteProviderServiceUserTool)] [RequirePermission(Permission.Tools_PromoteProviderServiceUser)] public IActionResult PromoteProviderServiceUser() { @@ -231,7 +229,6 @@ public class ToolsController : Controller [HttpPost] [ValidateAntiForgeryToken] - [RequireFeature(FeatureFlagKeys.PromoteProviderServiceUserTool)] [RequirePermission(Permission.Tools_PromoteProviderServiceUser)] public async Task PromoteProviderServiceUser(PromoteProviderServiceUserModel model) { diff --git a/src/Admin/Views/Shared/_Layout.cshtml b/src/Admin/Views/Shared/_Layout.cshtml index 361c1f9a57..1661a8bbc3 100644 --- a/src/Admin/Views/Shared/_Layout.cshtml +++ b/src/Admin/Views/Shared/_Layout.cshtml @@ -1,10 +1,8 @@ @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); @@ -13,8 +11,7 @@ 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 canPromoteProviderServiceUser = AccessControlService.UserHasPermission(Permission.Tools_PromoteProviderServiceUser); var canGenerateLicense = AccessControlService.UserHasPermission(Permission.Tools_GenerateLicenseFile); var canManageStripeSubscriptions = AccessControlService.UserHasPermission(Permission.Tools_ManageStripeSubscriptions); var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents); diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 62ce964001..c34303429c 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -157,7 +157,6 @@ 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 const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration"; public const string AuthenticatorSynciOS = "enable-authenticator-sync-ios"; public const string AuthenticatorSyncAndroid = "enable-authenticator-sync-android"; From a015f429c28636fce62e033e95d85ca83273c9f3 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:07:54 -0800 Subject: [PATCH 025/148] PM-12995 device exception cache permissions update (#5277) * feat(newDeviceVerification) : - adding more granular permissions for the login exception button. - fixed access to the button for different permissions --- src/Admin/Controllers/UsersController.cs | 2 +- src/Admin/Enums/Permissions.cs | 1 + src/Admin/Utilities/RolePermissionMapping.cs | 13 ++++++++----- src/Admin/Views/Users/Edit.cshtml | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Admin/Controllers/UsersController.cs b/src/Admin/Controllers/UsersController.cs index a988cc2af7..38e863aae7 100644 --- a/src/Admin/Controllers/UsersController.cs +++ b/src/Admin/Controllers/UsersController.cs @@ -165,7 +165,7 @@ public class UsersController : Controller [HttpPost] [ValidateAntiForgeryToken] - [RequirePermission(Permission.User_GeneralDetails_View)] + [RequirePermission(Permission.User_NewDeviceException_Edit)] [RequireFeature(FeatureFlagKeys.NewDeviceVerification)] public async Task ToggleNewDeviceVerification(Guid id) { diff --git a/src/Admin/Enums/Permissions.cs b/src/Admin/Enums/Permissions.cs index c544cb2106..20c500c061 100644 --- a/src/Admin/Enums/Permissions.cs +++ b/src/Admin/Enums/Permissions.cs @@ -17,6 +17,7 @@ public enum Permission User_Billing_View, User_Billing_Edit, User_Billing_LaunchGateway, + User_NewDeviceException_Edit, Org_List_View, Org_OrgInformation_View, diff --git a/src/Admin/Utilities/RolePermissionMapping.cs b/src/Admin/Utilities/RolePermissionMapping.cs index 381cf914aa..4b5a4e3802 100644 --- a/src/Admin/Utilities/RolePermissionMapping.cs +++ b/src/Admin/Utilities/RolePermissionMapping.cs @@ -12,7 +12,6 @@ public static class RolePermissionMapping Permission.User_List_View, Permission.User_UserInformation_View, Permission.User_GeneralDetails_View, - Permission.Org_CheckEnabledBox, Permission.User_Delete, Permission.User_UpgradePremium, Permission.User_BillingInformation_View, @@ -24,6 +23,8 @@ public static class RolePermissionMapping Permission.User_Billing_View, Permission.User_Billing_Edit, Permission.User_Billing_LaunchGateway, + Permission.User_NewDeviceException_Edit, + Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, Permission.Org_GeneralDetails_View, @@ -57,7 +58,6 @@ public static class RolePermissionMapping Permission.User_List_View, Permission.User_UserInformation_View, Permission.User_GeneralDetails_View, - Permission.Org_CheckEnabledBox, Permission.User_Delete, Permission.User_UpgradePremium, Permission.User_BillingInformation_View, @@ -70,6 +70,8 @@ public static class RolePermissionMapping Permission.User_Billing_View, Permission.User_Billing_Edit, Permission.User_Billing_LaunchGateway, + Permission.User_NewDeviceException_Edit, + Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, Permission.Org_GeneralDetails_View, @@ -106,7 +108,6 @@ public static class RolePermissionMapping Permission.User_List_View, Permission.User_UserInformation_View, Permission.User_GeneralDetails_View, - Permission.Org_CheckEnabledBox, Permission.User_UpgradePremium, Permission.User_BillingInformation_View, Permission.User_BillingInformation_DownloadInvoice, @@ -114,6 +115,8 @@ public static class RolePermissionMapping Permission.User_Licensing_View, Permission.User_Billing_View, Permission.User_Billing_LaunchGateway, + Permission.User_NewDeviceException_Edit, + Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, Permission.Org_GeneralDetails_View, @@ -135,7 +138,6 @@ public static class RolePermissionMapping Permission.User_List_View, Permission.User_UserInformation_View, Permission.User_GeneralDetails_View, - Permission.Org_CheckEnabledBox, Permission.User_UpgradePremium, Permission.User_BillingInformation_View, Permission.User_BillingInformation_DownloadInvoice, @@ -146,6 +148,7 @@ public static class RolePermissionMapping Permission.User_Billing_View, Permission.User_Billing_Edit, Permission.User_Billing_LaunchGateway, + Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, Permission.Org_GeneralDetails_View, @@ -177,12 +180,12 @@ public static class RolePermissionMapping Permission.User_List_View, Permission.User_UserInformation_View, Permission.User_GeneralDetails_View, - Permission.Org_CheckEnabledBox, Permission.User_BillingInformation_View, Permission.User_BillingInformation_DownloadInvoice, Permission.User_Premium_View, Permission.User_Licensing_View, Permission.User_Licensing_Edit, + Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, Permission.Org_GeneralDetails_View, diff --git a/src/Admin/Views/Users/Edit.cshtml b/src/Admin/Views/Users/Edit.cshtml index 417d9fb9a2..495fc43c2f 100644 --- a/src/Admin/Views/Users/Edit.cshtml +++ b/src/Admin/Views/Users/Edit.cshtml @@ -7,7 +7,7 @@ ViewData["Title"] = "User: " + Model.User.Email; var canViewUserInformation = AccessControlService.UserHasPermission(Permission.User_UserInformation_View); - var canViewNewDeviceException = AccessControlService.UserHasPermission(Permission.User_UserInformation_View) && + var canViewNewDeviceException = AccessControlService.UserHasPermission(Permission.User_NewDeviceException_Edit) && FeatureService.IsEnabled(Bit.Core.FeatureFlagKeys.NewDeviceVerification); var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.User_BillingInformation_View); var canViewGeneral = AccessControlService.UserHasPermission(Permission.User_GeneralDetails_View); From d8b4a4a28d40c2e5ab5571d35017c18fb5dc2ff2 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Thu, 16 Jan 2025 14:35:00 -0500 Subject: [PATCH 026/148] Drop `LimitCollectionCreationDeletion` from the database (#4810) * Drop a MSSQL column * Delete property from `Organization` entity * Generate EF migrations --- .../AdminConsole/Models/Organization.cs | 6 - .../Stored Procedures/Organization_Create.sql | 6 - .../Organization_ReadAbilities.sql | 1 - .../Stored Procedures/Organization_Update.sql | 5 - src/Sql/dbo/Tables/Organization.sql | 1 - ...rganizationUserOrganizationDetailsView.sql | 1 - ...derUserProviderOrganizationDetailsView.sql | 1 - ...izationLimitCollectionCreationDeletion.sql | 485 +++ ...imitCollectionCreationDeletion.Designer.cs | 2994 ++++++++++++++++ ...214_DropLimitCollectionCreationDeletion.cs | 28 + .../DatabaseContextModelSnapshot.cs | 63 +- ...imitCollectionCreationDeletion.Designer.cs | 3000 +++++++++++++++++ ...219_DropLimitCollectionCreationDeletion.cs | 28 + .../DatabaseContextModelSnapshot.cs | 63 +- ...imitCollectionCreationDeletion.Designer.cs | 2983 ++++++++++++++++ ...222_DropLimitCollectionCreationDeletion.cs | 28 + .../DatabaseContextModelSnapshot.cs | 63 +- 17 files changed, 9636 insertions(+), 120 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-01-16_00_DropOrganizationLimitCollectionCreationDeletion.sql create mode 100644 util/MySqlMigrations/Migrations/20250116163214_DropLimitCollectionCreationDeletion.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250116163214_DropLimitCollectionCreationDeletion.cs create mode 100644 util/PostgresMigrations/Migrations/20250116163219_DropLimitCollectionCreationDeletion.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250116163219_DropLimitCollectionCreationDeletion.cs create mode 100644 util/SqliteMigrations/Migrations/20250116163222_DropLimitCollectionCreationDeletion.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250116163222_DropLimitCollectionCreationDeletion.cs diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Models/Organization.cs b/src/Infrastructure.EntityFramework/AdminConsole/Models/Organization.cs index 39968142bb..d7f83d829d 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Models/Organization.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Models/Organization.cs @@ -9,11 +9,6 @@ namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models; public class Organization : Core.AdminConsole.Entities.Organization { - // Shadow property. To be removed by - // https://bitwarden.atlassian.net/browse/PM-10863. - // This was replaced with `LimitCollectionCreation` and - // `LimitCollectionDeletion`. - public bool LimitCollectionCreationDeletion { get; set; } public virtual ICollection Ciphers { get; set; } public virtual ICollection OrganizationUsers { get; set; } public virtual ICollection Groups { get; set; } @@ -43,7 +38,6 @@ 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/Sql/dbo/Stored Procedures/Organization_Create.sql b/src/Sql/dbo/Stored Procedures/Organization_Create.sql index d33269063f..9f12a3f347 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Create.sql @@ -51,7 +51,6 @@ CREATE PROCEDURE [dbo].[Organization_Create] @MaxAutoscaleSmSeats INT= null, @MaxAutoscaleSmServiceAccounts INT = null, @SecretsManagerBeta BIT = 0, - @LimitCollectionCreationDeletion BIT = NULL, -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 @LimitCollectionCreation BIT = NULL, @LimitCollectionDeletion BIT = NULL, @AllowAdminAccessToAllCollectionItems BIT = 0, @@ -60,9 +59,6 @@ AS BEGIN SET NOCOUNT ON - SET @LimitCollectionCreation = COALESCE(@LimitCollectionCreation, @LimitCollectionCreationDeletion, 0); - SET @LimitCollectionDeletion = COALESCE(@LimitCollectionDeletion, @LimitCollectionCreationDeletion, 0); - INSERT INTO [dbo].[Organization] ( [Id], @@ -117,7 +113,6 @@ BEGIN [MaxAutoscaleSmSeats], [MaxAutoscaleSmServiceAccounts], [SecretsManagerBeta], - [LimitCollectionCreationDeletion], -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 [LimitCollectionCreation], [LimitCollectionDeletion], [AllowAdminAccessToAllCollectionItems], @@ -177,7 +172,6 @@ BEGIN @MaxAutoscaleSmSeats, @MaxAutoscaleSmServiceAccounts, @SecretsManagerBeta, - COALESCE(@LimitCollectionCreation, @LimitCollectionDeletion, 0), -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863) @LimitCollectionCreation, @LimitCollectionDeletion, @AllowAdminAccessToAllCollectionItems, diff --git a/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql b/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql index 056bc1416c..5959742dae 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql @@ -21,7 +21,6 @@ BEGIN [UseResetPassword], [UsePolicies], [Enabled], - [LimitCollectionCreationDeletion], -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 [LimitCollectionCreation], [LimitCollectionDeletion], [AllowAdminAccessToAllCollectionItems], diff --git a/src/Sql/dbo/Stored Procedures/Organization_Update.sql b/src/Sql/dbo/Stored Procedures/Organization_Update.sql index 1bbcb7ebc8..a1af26851e 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Update.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Update.sql @@ -51,7 +51,6 @@ CREATE PROCEDURE [dbo].[Organization_Update] @MaxAutoscaleSmSeats INT = null, @MaxAutoscaleSmServiceAccounts INT = null, @SecretsManagerBeta BIT = 0, - @LimitCollectionCreationDeletion BIT = null, -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 @LimitCollectionCreation BIT = null, @LimitCollectionDeletion BIT = null, @AllowAdminAccessToAllCollectionItems BIT = 0, @@ -60,9 +59,6 @@ AS BEGIN SET NOCOUNT ON - SET @LimitCollectionCreation = COALESCE(@LimitCollectionCreation, @LimitCollectionCreationDeletion, 0); - SET @LimitCollectionDeletion = COALESCE(@LimitCollectionDeletion, @LimitCollectionCreationDeletion, 0); - UPDATE [dbo].[Organization] SET @@ -117,7 +113,6 @@ BEGIN [MaxAutoscaleSmSeats] = @MaxAutoscaleSmSeats, [MaxAutoscaleSmServiceAccounts] = @MaxAutoscaleSmServiceAccounts, [SecretsManagerBeta] = @SecretsManagerBeta, - [LimitCollectionCreationDeletion] = COALESCE(@LimitCollectionCreation, @LimitCollectionDeletion, 0), [LimitCollectionCreation] = @LimitCollectionCreation, [LimitCollectionDeletion] = @LimitCollectionDeletion, [AllowAdminAccessToAllCollectionItems] = @AllowAdminAccessToAllCollectionItems, diff --git a/src/Sql/dbo/Tables/Organization.sql b/src/Sql/dbo/Tables/Organization.sql index 279b78bfc1..2178494c19 100644 --- a/src/Sql/dbo/Tables/Organization.sql +++ b/src/Sql/dbo/Tables/Organization.sql @@ -51,7 +51,6 @@ CREATE TABLE [dbo].[Organization] ( [MaxAutoscaleSmSeats] INT NULL, [MaxAutoscaleSmServiceAccounts] INT NULL, [SecretsManagerBeta] BIT NOT NULL CONSTRAINT [DF_Organization_SecretsManagerBeta] DEFAULT (0), - [LimitCollectionCreationDeletion] BIT NOT NULL CONSTRAINT [DF_Organization_LimitCollectionCreationDeletion] DEFAULT (0), [LimitCollectionCreation] BIT NOT NULL CONSTRAINT [DF_Organization_LimitCollectionCreation] DEFAULT (0), [LimitCollectionDeletion] BIT NOT NULL CONSTRAINT [DF_Organization_LimitCollectionDeletion] DEFAULT (0), [AllowAdminAccessToAllCollectionItems] BIT NOT NULL CONSTRAINT [DF_Organization_AllowAdminAccessToAllCollectionItems] DEFAULT (0), diff --git a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql index c4f79e0c69..fc7ab1d31a 100644 --- a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql +++ b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql @@ -46,7 +46,6 @@ SELECT O.[UsePasswordManager], O.[SmSeats], O.[SmServiceAccounts], - O.[LimitCollectionCreationDeletion], -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 O.[LimitCollectionCreation], O.[LimitCollectionDeletion], O.[AllowAdminAccessToAllCollectionItems], diff --git a/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql b/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql index 20b896b6ad..4915a406a1 100644 --- a/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql +++ b/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql @@ -32,7 +32,6 @@ SELECT PU.[Id] ProviderUserId, P.[Name] ProviderName, O.[PlanType], - O.[LimitCollectionCreationDeletion], -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 O.[LimitCollectionCreation], O.[LimitCollectionDeletion], O.[AllowAdminAccessToAllCollectionItems], diff --git a/util/Migrator/DbScripts/2025-01-16_00_DropOrganizationLimitCollectionCreationDeletion.sql b/util/Migrator/DbScripts/2025-01-16_00_DropOrganizationLimitCollectionCreationDeletion.sql new file mode 100644 index 0000000000..34a14900f6 --- /dev/null +++ b/util/Migrator/DbScripts/2025-01-16_00_DropOrganizationLimitCollectionCreationDeletion.sql @@ -0,0 +1,485 @@ +-- Finalise removal of Organization.LimitCollectionCreationDeletion column + +-- Drop default constraint +IF OBJECT_ID('[dbo].[DF_Organization_LimitCollectionCreationDeletion]', 'D') IS NOT NULL +BEGIN + ALTER TABLE + [dbo].[Organization] + DROP CONSTRAINT + [DF_Organization_LimitCollectionCreationDeletion] +END +GO + +-- Drop the column +IF COL_LENGTH('[dbo].[Organization]', 'LimitCollectionCreationDeletion') IS NOT NULL +BEGIN + ALTER TABLE + [dbo].[Organization] + DROP COLUMN + [LimitCollectionCreationDeletion] +END +GO + +-- Refresh Views +CREATE OR ALTER VIEW [dbo].[OrganizationUserOrganizationDetailsView] +AS +SELECT + OU.[UserId], + OU.[OrganizationId], + OU.[Id] OrganizationUserId, + O.[Name], + O.[Enabled], + O.[PlanType], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseScim], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[UseCustomPermissions], + O.[UseSecretsManager], + O.[Seats], + O.[MaxCollections], + O.[MaxStorageGb], + O.[Identifier], + OU.[Key], + OU.[ResetPasswordKey], + O.[PublicKey], + O.[PrivateKey], + OU.[Status], + OU.[Type], + SU.[ExternalId] SsoExternalId, + OU.[Permissions], + PO.[ProviderId], + P.[Name] ProviderName, + P.[Type] ProviderType, + SS.[Data] SsoConfig, + OS.[FriendlyName] FamilySponsorshipFriendlyName, + OS.[LastSyncDate] FamilySponsorshipLastSyncDate, + OS.[ToDelete] FamilySponsorshipToDelete, + OS.[ValidUntil] FamilySponsorshipValidUntil, + OU.[AccessSecretsManager], + O.[UsePasswordManager], + O.[SmSeats], + O.[SmServiceAccounts], + O.[LimitCollectionCreation], + O.[LimitCollectionDeletion], + O.[AllowAdminAccessToAllCollectionItems], + O.[UseRiskInsights] +FROM + [dbo].[OrganizationUser] OU +LEFT JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] +LEFT JOIN + [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id] +LEFT JOIN + [dbo].[Provider] P ON P.[Id] = PO.[ProviderId] +LEFT JOIN + [dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserID] = OU.[Id] +GO + +CREATE OR ALTER VIEW [dbo].[ProviderUserProviderOrganizationDetailsView] +AS +SELECT + PU.[UserId], + PO.[OrganizationId], + O.[Name], + O.[Enabled], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseScim], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[UseCustomPermissions], + O.[Seats], + O.[MaxCollections], + O.[MaxStorageGb], + O.[Identifier], + PO.[Key], + O.[PublicKey], + O.[PrivateKey], + PU.[Status], + PU.[Type], + PO.[ProviderId], + PU.[Id] ProviderUserId, + P.[Name] ProviderName, + O.[PlanType], + O.[LimitCollectionCreation], + O.[LimitCollectionDeletion], + O.[AllowAdminAccessToAllCollectionItems], + O.[UseRiskInsights], + P.[Type] ProviderType +FROM + [dbo].[ProviderUser] PU +INNER JOIN + [dbo].[ProviderOrganization] PO ON PO.[ProviderId] = PU.[ProviderId] +INNER JOIN + [dbo].[Organization] O ON O.[Id] = PO.[OrganizationId] +INNER JOIN + [dbo].[Provider] P ON P.[Id] = PU.[ProviderId] +GO + +-- Refresh Stored Procedures +CREATE OR ALTER PROCEDURE [dbo].[Organization_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @Identifier NVARCHAR(50), + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @Plan NVARCHAR(50), + @PlanType TINYINT, + @Seats INT, + @MaxCollections SMALLINT, + @UsePolicies BIT, + @UseSso BIT, + @UseGroups BIT, + @UseDirectory BIT, + @UseEvents BIT, + @UseTotp BIT, + @Use2fa BIT, + @UseApi BIT, + @UseResetPassword BIT, + @SelfHost BIT, + @UsersGetPremium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @Enabled BIT, + @LicenseKey VARCHAR(100), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), + @TwoFactorProviders NVARCHAR(MAX), + @ExpirationDate DATETIME2(7), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @OwnersNotifiedOfAutoscaling DATETIME2(7), + @MaxAutoscaleSeats INT, + @UseKeyConnector BIT = 0, + @UseScim BIT = 0, + @UseCustomPermissions BIT = 0, + @UseSecretsManager BIT = 0, + @Status TINYINT = 0, + @UsePasswordManager BIT = 1, + @SmSeats INT = null, + @SmServiceAccounts INT = null, + @MaxAutoscaleSmSeats INT= null, + @MaxAutoscaleSmServiceAccounts INT = null, + @SecretsManagerBeta BIT = 0, + @LimitCollectionCreation BIT = NULL, + @LimitCollectionDeletion BIT = NULL, + @AllowAdminAccessToAllCollectionItems BIT = 0, + @UseRiskInsights BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Organization] + ( + [Id], + [Identifier], + [Name], + [BusinessName], + [BusinessAddress1], + [BusinessAddress2], + [BusinessAddress3], + [BusinessCountry], + [BusinessTaxNumber], + [BillingEmail], + [Plan], + [PlanType], + [Seats], + [MaxCollections], + [UsePolicies], + [UseSso], + [UseGroups], + [UseDirectory], + [UseEvents], + [UseTotp], + [Use2fa], + [UseApi], + [UseResetPassword], + [SelfHost], + [UsersGetPremium], + [Storage], + [MaxStorageGb], + [Gateway], + [GatewayCustomerId], + [GatewaySubscriptionId], + [ReferenceData], + [Enabled], + [LicenseKey], + [PublicKey], + [PrivateKey], + [TwoFactorProviders], + [ExpirationDate], + [CreationDate], + [RevisionDate], + [OwnersNotifiedOfAutoscaling], + [MaxAutoscaleSeats], + [UseKeyConnector], + [UseScim], + [UseCustomPermissions], + [UseSecretsManager], + [Status], + [UsePasswordManager], + [SmSeats], + [SmServiceAccounts], + [MaxAutoscaleSmSeats], + [MaxAutoscaleSmServiceAccounts], + [SecretsManagerBeta], + [LimitCollectionCreation], + [LimitCollectionDeletion], + [AllowAdminAccessToAllCollectionItems], + [UseRiskInsights] + ) + VALUES + ( + @Id, + @Identifier, + @Name, + @BusinessName, + @BusinessAddress1, + @BusinessAddress2, + @BusinessAddress3, + @BusinessCountry, + @BusinessTaxNumber, + @BillingEmail, + @Plan, + @PlanType, + @Seats, + @MaxCollections, + @UsePolicies, + @UseSso, + @UseGroups, + @UseDirectory, + @UseEvents, + @UseTotp, + @Use2fa, + @UseApi, + @UseResetPassword, + @SelfHost, + @UsersGetPremium, + @Storage, + @MaxStorageGb, + @Gateway, + @GatewayCustomerId, + @GatewaySubscriptionId, + @ReferenceData, + @Enabled, + @LicenseKey, + @PublicKey, + @PrivateKey, + @TwoFactorProviders, + @ExpirationDate, + @CreationDate, + @RevisionDate, + @OwnersNotifiedOfAutoscaling, + @MaxAutoscaleSeats, + @UseKeyConnector, + @UseScim, + @UseCustomPermissions, + @UseSecretsManager, + @Status, + @UsePasswordManager, + @SmSeats, + @SmServiceAccounts, + @MaxAutoscaleSmSeats, + @MaxAutoscaleSmServiceAccounts, + @SecretsManagerBeta, + @LimitCollectionCreation, + @LimitCollectionDeletion, + @AllowAdminAccessToAllCollectionItems, + @UseRiskInsights + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Organization_ReadAbilities] +AS +BEGIN + SET NOCOUNT ON + + SELECT + [Id], + [UseEvents], + [Use2fa], + CASE + WHEN [Use2fa] = 1 AND [TwoFactorProviders] IS NOT NULL AND [TwoFactorProviders] != '{}' THEN + 1 + ELSE + 0 + END AS [Using2fa], + [UsersGetPremium], + [UseCustomPermissions], + [UseSso], + [UseKeyConnector], + [UseScim], + [UseResetPassword], + [UsePolicies], + [Enabled], + [LimitCollectionCreation], + [LimitCollectionDeletion], + [AllowAdminAccessToAllCollectionItems], + [UseRiskInsights] + FROM + [dbo].[Organization] +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Organization_Update] + @Id UNIQUEIDENTIFIER, + @Identifier NVARCHAR(50), + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @Plan NVARCHAR(50), + @PlanType TINYINT, + @Seats INT, + @MaxCollections SMALLINT, + @UsePolicies BIT, + @UseSso BIT, + @UseGroups BIT, + @UseDirectory BIT, + @UseEvents BIT, + @UseTotp BIT, + @Use2fa BIT, + @UseApi BIT, + @UseResetPassword BIT, + @SelfHost BIT, + @UsersGetPremium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @Enabled BIT, + @LicenseKey VARCHAR(100), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), + @TwoFactorProviders NVARCHAR(MAX), + @ExpirationDate DATETIME2(7), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @OwnersNotifiedOfAutoscaling DATETIME2(7), + @MaxAutoscaleSeats INT, + @UseKeyConnector BIT = 0, + @UseScim BIT = 0, + @UseCustomPermissions BIT = 0, + @UseSecretsManager BIT = 0, + @Status TINYINT = 0, + @UsePasswordManager BIT = 1, + @SmSeats INT = null, + @SmServiceAccounts INT = null, + @MaxAutoscaleSmSeats INT = null, + @MaxAutoscaleSmServiceAccounts INT = null, + @SecretsManagerBeta BIT = 0, + @LimitCollectionCreation BIT = null, + @LimitCollectionDeletion BIT = null, + @AllowAdminAccessToAllCollectionItems BIT = 0, + @UseRiskInsights BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Organization] + SET + [Identifier] = @Identifier, + [Name] = @Name, + [BusinessName] = @BusinessName, + [BusinessAddress1] = @BusinessAddress1, + [BusinessAddress2] = @BusinessAddress2, + [BusinessAddress3] = @BusinessAddress3, + [BusinessCountry] = @BusinessCountry, + [BusinessTaxNumber] = @BusinessTaxNumber, + [BillingEmail] = @BillingEmail, + [Plan] = @Plan, + [PlanType] = @PlanType, + [Seats] = @Seats, + [MaxCollections] = @MaxCollections, + [UsePolicies] = @UsePolicies, + [UseSso] = @UseSso, + [UseGroups] = @UseGroups, + [UseDirectory] = @UseDirectory, + [UseEvents] = @UseEvents, + [UseTotp] = @UseTotp, + [Use2fa] = @Use2fa, + [UseApi] = @UseApi, + [UseResetPassword] = @UseResetPassword, + [SelfHost] = @SelfHost, + [UsersGetPremium] = @UsersGetPremium, + [Storage] = @Storage, + [MaxStorageGb] = @MaxStorageGb, + [Gateway] = @Gateway, + [GatewayCustomerId] = @GatewayCustomerId, + [GatewaySubscriptionId] = @GatewaySubscriptionId, + [ReferenceData] = @ReferenceData, + [Enabled] = @Enabled, + [LicenseKey] = @LicenseKey, + [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey, + [TwoFactorProviders] = @TwoFactorProviders, + [ExpirationDate] = @ExpirationDate, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [OwnersNotifiedOfAutoscaling] = @OwnersNotifiedOfAutoscaling, + [MaxAutoscaleSeats] = @MaxAutoscaleSeats, + [UseKeyConnector] = @UseKeyConnector, + [UseScim] = @UseScim, + [UseCustomPermissions] = @UseCustomPermissions, + [UseSecretsManager] = @UseSecretsManager, + [Status] = @Status, + [UsePasswordManager] = @UsePasswordManager, + [SmSeats] = @SmSeats, + [SmServiceAccounts] = @SmServiceAccounts, + [MaxAutoscaleSmSeats] = @MaxAutoscaleSmSeats, + [MaxAutoscaleSmServiceAccounts] = @MaxAutoscaleSmServiceAccounts, + [SecretsManagerBeta] = @SecretsManagerBeta, + [LimitCollectionCreation] = @LimitCollectionCreation, + [LimitCollectionDeletion] = @LimitCollectionDeletion, + [AllowAdminAccessToAllCollectionItems] = @AllowAdminAccessToAllCollectionItems, + [UseRiskInsights] = @UseRiskInsights + WHERE + [Id] = @Id +END +GO + +CREATE OR ALTER VIEW [dbo].[OrganizationView] +AS +SELECT + * +FROM + [dbo].[Organization] diff --git a/util/MySqlMigrations/Migrations/20250116163214_DropLimitCollectionCreationDeletion.Designer.cs b/util/MySqlMigrations/Migrations/20250116163214_DropLimitCollectionCreationDeletion.Designer.cs new file mode 100644 index 0000000000..8431d0d4ed --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250116163214_DropLimitCollectionCreationDeletion.Designer.cs @@ -0,0 +1,2994 @@ +// +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("20250116163214_DropLimitCollectionCreationDeletion")] + partial class DropLimitCollectionCreationDeletion + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("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.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("VerifyDevices") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .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.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.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/20250116163214_DropLimitCollectionCreationDeletion.cs b/util/MySqlMigrations/Migrations/20250116163214_DropLimitCollectionCreationDeletion.cs new file mode 100644 index 0000000000..3248d1df9d --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250116163214_DropLimitCollectionCreationDeletion.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class DropLimitCollectionCreationDeletion : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LimitCollectionCreationDeletion", + table: "Organization"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LimitCollectionCreationDeletion", + table: "Organization", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index dcc525c433..46d2a2e5fd 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -91,9 +91,6 @@ namespace Bit.MySqlMigrations.Migrations b.Property("LimitCollectionCreation") .HasColumnType("tinyint(1)"); - b.Property("LimitCollectionCreationDeletion") - .HasColumnType("tinyint(1)"); - b.Property("LimitCollectionDeletion") .HasColumnType("tinyint(1)"); @@ -1144,35 +1141,6 @@ namespace Bit.MySqlMigrations.Migrations 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") @@ -1748,6 +1716,35 @@ namespace Bit.MySqlMigrations.Migrations b.ToTable("NotificationStatus", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => { b.Property("Id") @@ -2373,7 +2370,7 @@ namespace Bit.MySqlMigrations.Migrations modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => { - b.HasOne("Bit.Infrastructure.EntityFramework.Models.Installation", "Installation") + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") .WithMany() .HasForeignKey("InstallationId") .OnDelete(DeleteBehavior.Cascade) diff --git a/util/PostgresMigrations/Migrations/20250116163219_DropLimitCollectionCreationDeletion.Designer.cs b/util/PostgresMigrations/Migrations/20250116163219_DropLimitCollectionCreationDeletion.Designer.cs new file mode 100644 index 0000000000..e88fa1485c --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250116163219_DropLimitCollectionCreationDeletion.Designer.cs @@ -0,0 +1,3000 @@ +// +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("20250116163219_DropLimitCollectionCreationDeletion")] + partial class DropLimitCollectionCreationDeletion + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("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.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property("VerifyDevices") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .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.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.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/20250116163219_DropLimitCollectionCreationDeletion.cs b/util/PostgresMigrations/Migrations/20250116163219_DropLimitCollectionCreationDeletion.cs new file mode 100644 index 0000000000..677dec53ac --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250116163219_DropLimitCollectionCreationDeletion.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class DropLimitCollectionCreationDeletion : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LimitCollectionCreationDeletion", + table: "Organization"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LimitCollectionCreationDeletion", + table: "Organization", + type: "boolean", + nullable: false, + defaultValue: false); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 971ba96310..29672b80a9 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -93,9 +93,6 @@ namespace Bit.PostgresMigrations.Migrations b.Property("LimitCollectionCreation") .HasColumnType("boolean"); - b.Property("LimitCollectionCreationDeletion") - .HasColumnType("boolean"); - b.Property("LimitCollectionDeletion") .HasColumnType("boolean"); @@ -1149,35 +1146,6 @@ namespace Bit.PostgresMigrations.Migrations 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") @@ -1754,6 +1722,35 @@ namespace Bit.PostgresMigrations.Migrations b.ToTable("NotificationStatus", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => { b.Property("Id") @@ -2379,7 +2376,7 @@ namespace Bit.PostgresMigrations.Migrations modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => { - b.HasOne("Bit.Infrastructure.EntityFramework.Models.Installation", "Installation") + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") .WithMany() .HasForeignKey("InstallationId") .OnDelete(DeleteBehavior.Cascade) diff --git a/util/SqliteMigrations/Migrations/20250116163222_DropLimitCollectionCreationDeletion.Designer.cs b/util/SqliteMigrations/Migrations/20250116163222_DropLimitCollectionCreationDeletion.Designer.cs new file mode 100644 index 0000000000..53111def25 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250116163222_DropLimitCollectionCreationDeletion.Designer.cs @@ -0,0 +1,2983 @@ +// +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("20250116163222_DropLimitCollectionCreationDeletion")] + partial class DropLimitCollectionCreationDeletion + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("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.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("VerifyDevices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .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.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.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/20250116163222_DropLimitCollectionCreationDeletion.cs b/util/SqliteMigrations/Migrations/20250116163222_DropLimitCollectionCreationDeletion.cs new file mode 100644 index 0000000000..34275a491e --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250116163222_DropLimitCollectionCreationDeletion.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class DropLimitCollectionCreationDeletion : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LimitCollectionCreationDeletion", + table: "Organization"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LimitCollectionCreationDeletion", + table: "Organization", + type: "INTEGER", + nullable: false, + defaultValue: false); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index d9be32398b..d71327ec3c 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -86,9 +86,6 @@ namespace Bit.SqliteMigrations.Migrations b.Property("LimitCollectionCreation") .HasColumnType("INTEGER"); - b.Property("LimitCollectionCreationDeletion") - .HasColumnType("INTEGER"); - b.Property("LimitCollectionDeletion") .HasColumnType("INTEGER"); @@ -1133,35 +1130,6 @@ namespace Bit.SqliteMigrations.Migrations 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") @@ -1737,6 +1705,35 @@ namespace Bit.SqliteMigrations.Migrations b.ToTable("NotificationStatus", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => { b.Property("Id") @@ -2362,7 +2359,7 @@ namespace Bit.SqliteMigrations.Migrations modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => { - b.HasOne("Bit.Infrastructure.EntityFramework.Models.Installation", "Installation") + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") .WithMany() .HasForeignKey("InstallationId") .OnDelete(DeleteBehavior.Cascade) From 677265b1e1c34f36a7192668613c779c9726fd78 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:27:48 -0500 Subject: [PATCH 027/148] [PM-17177] Added additional validation to ensure license claim values aren't null (#5280) * Added additional validation to ensure license claim values aren't null * Added extra not null validation for any property with a type that can possibly be null --- .../OrganizationLicenseClaimsFactory.cs | 62 +++++++++++++++---- .../UserLicenseClaimsFactory.cs | 22 ++++--- 2 files changed, 66 insertions(+), 18 deletions(-) diff --git a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs index 1aac7bb1d8..e436102012 100644 --- a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs +++ b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs @@ -22,16 +22,9 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory { 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()), @@ -43,32 +36,79 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory { new(nameof(UserLicenseConstants.LicenseType), LicenseType.User.ToString()), 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.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)), new(nameof(UserLicenseConstants.Trial), trial.ToString()), }; + if (entity.Email is not null) + { + claims.Add(new(nameof(UserLicenseConstants.Email), entity.Email)); + } + + if (entity.Name is not null) + { + claims.Add(new(nameof(UserLicenseConstants.Name), entity.Name)); + } + if (entity.LicenseKey is not null) { claims.Add(new(nameof(UserLicenseConstants.LicenseKey), entity.LicenseKey)); } - if (entity.MaxStorageGb is not null) + if (entity.MaxStorageGb.HasValue) { claims.Add(new(nameof(UserLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString())); } - if (expires is not null) + if (expires.HasValue) { - claims.Add(new(nameof(UserLicenseConstants.Expires), expires.ToString())); + claims.Add(new(nameof(UserLicenseConstants.Expires), expires.Value.ToString(CultureInfo.InvariantCulture))); } - if (refresh is not null) + if (refresh.HasValue) { - claims.Add(new(nameof(UserLicenseConstants.Refresh), refresh.ToString())); + claims.Add(new(nameof(UserLicenseConstants.Refresh), refresh.Value.ToString(CultureInfo.InvariantCulture))); } return Task.FromResult(claims); From 0c29e9227c9fbf0946793373d21e3dc1439299d3 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 17 Jan 2025 08:28:23 +1000 Subject: [PATCH 028/148] Remove provider-export-permission feature flag (#5263) * also remove old CipherService and CollectionService methods only used by old export code --- .../OrganizationExportController.cs | 55 +------------------ src/Core/Constants.cs | 1 - src/Core/Context/ICurrentContext.cs | 2 + src/Core/Services/ICollectionService.cs | 2 - .../Implementations/CollectionService.cs | 27 --------- src/Core/Vault/Services/ICipherService.cs | 1 - .../Services/Implementations/CipherService.cs | 29 ---------- .../Services/CollectionServiceTests.cs | 30 ---------- 8 files changed, 3 insertions(+), 144 deletions(-) diff --git a/src/Api/Tools/Controllers/OrganizationExportController.cs b/src/Api/Tools/Controllers/OrganizationExportController.cs index 144e1be69e..520746f139 100644 --- a/src/Api/Tools/Controllers/OrganizationExportController.cs +++ b/src/Api/Tools/Controllers/OrganizationExportController.cs @@ -1,16 +1,11 @@ -using Bit.Api.Models.Response; -using Bit.Api.Tools.Authorization; +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; @@ -56,39 +51,6 @@ public class OrganizationExportController : Controller [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); - (IEnumerable orgCiphers, Dictionary> collectionCiphersGroupDict) = await _cipherService.GetOrganizationCiphers(userId, organizationId); - - if (_currentContext.ClientVersion == null || _currentContext.ClientVersion >= new Version("2023.1.0")) - { - var organizationExportResponseModel = new OrganizationExportResponseModel - { - Collections = orgCollections.Select(c => new CollectionResponseModel(c)), - Ciphers = orgCiphers.Select(c => new CipherMiniDetailsResponseModel(c, _globalSettings, collectionCiphersGroupDict, c.OrganizationUseTotp)) - }; - - return Ok(organizationExportResponseModel); - } - - // Backward compatibility with versions before 2023.1.0 that use ListResponseModel - var organizationExportListResponseModel = new OrganizationExportListResponseModel - { - Collections = GetOrganizationCollectionsResponse(orgCollections), - Ciphers = GetOrganizationCiphersResponse(orgCiphers, collectionCiphersGroupDict) - }; - - return Ok(organizationExportListResponseModel); - } - - private async Task Export_vNext(Guid organizationId) { var canExportAll = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(organizationId), VaultExportOperations.ExportWholeVault); @@ -116,19 +78,4 @@ public class OrganizationExportController : Controller // Unauthorized throw new NotFoundException(); } - - private ListResponseModel GetOrganizationCollectionsResponse(IEnumerable orgCollections) - { - var collections = orgCollections.Select(c => new CollectionResponseModel(c)); - return new ListResponseModel(collections); - } - - private ListResponseModel GetOrganizationCiphersResponse(IEnumerable orgCiphers, - Dictionary> collectionCiphersGroupDict) - { - var responses = orgCiphers.Select(c => new CipherMiniDetailsResponseModel(c, _globalSettings, - collectionCiphersGroupDict, c.OrganizationUseTotp)); - - return new ListResponseModel(responses); - } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index c34303429c..656e943dc0 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -150,7 +150,6 @@ public static class FeatureFlagKeys public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss"; public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss"; public const string SecurityTasks = "security-tasks"; - 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/Context/ICurrentContext.cs b/src/Core/Context/ICurrentContext.cs index 3d3a5960b7..9361480229 100644 --- a/src/Core/Context/ICurrentContext.cs +++ b/src/Core/Context/ICurrentContext.cs @@ -43,7 +43,9 @@ public interface ICurrentContext Task AccessEventLogs(Guid orgId); Task AccessImportExport(Guid orgId); Task AccessReports(Guid orgId); + [Obsolete("Deprecated. Use an authorization handler checking the specific permissions required instead.")] Task EditAnyCollection(Guid orgId); + [Obsolete("Deprecated. Use an authorization handler checking the specific permissions required instead.")] Task ViewAllCollections(Guid orgId); Task ManageGroups(Guid orgId); Task ManagePolicies(Guid orgId); diff --git a/src/Core/Services/ICollectionService.cs b/src/Core/Services/ICollectionService.cs index 27c4118197..c116e5f076 100644 --- a/src/Core/Services/ICollectionService.cs +++ b/src/Core/Services/ICollectionService.cs @@ -7,6 +7,4 @@ public interface ICollectionService { Task SaveAsync(Collection collection, IEnumerable groups = null, IEnumerable users = null); Task DeleteUserAsync(Collection collection, Guid organizationUserId); - [Obsolete("Pre-Flexible Collections logic.")] - Task> GetOrganizationCollectionsAsync(Guid organizationId); } diff --git a/src/Core/Services/Implementations/CollectionService.cs b/src/Core/Services/Implementations/CollectionService.cs index e779ac289f..f6e9735f4e 100644 --- a/src/Core/Services/Implementations/CollectionService.cs +++ b/src/Core/Services/Implementations/CollectionService.cs @@ -95,31 +95,4 @@ public class CollectionService : ICollectionService await _collectionRepository.DeleteUserAsync(collection.Id, organizationUserId); await _eventService.LogOrganizationUserEventAsync(orgUser, Enums.EventType.OrganizationUser_Updated); } - - public async Task> GetOrganizationCollectionsAsync(Guid organizationId) - { - if ( - !await _currentContext.ViewAllCollections(organizationId) && - !await _currentContext.ManageUsers(organizationId) && - !await _currentContext.ManageGroups(organizationId) && - !await _currentContext.AccessImportExport(organizationId) - ) - { - throw new NotFoundException(); - } - - IEnumerable orgCollections; - if (await _currentContext.ViewAllCollections(organizationId) || await _currentContext.AccessImportExport(organizationId)) - { - // Admins, Owners, Providers and Custom (with collection management or import/export permissions) can access all items even if not assigned to them - orgCollections = await _collectionRepository.GetManyByOrganizationIdAsync(organizationId); - } - else - { - var collections = await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId.Value); - orgCollections = collections.Where(c => c.OrganizationId == organizationId); - } - - return orgCollections; - } } diff --git a/src/Core/Vault/Services/ICipherService.cs b/src/Core/Vault/Services/ICipherService.cs index 83cd729e13..27b84e4a47 100644 --- a/src/Core/Vault/Services/ICipherService.cs +++ b/src/Core/Vault/Services/ICipherService.cs @@ -39,5 +39,4 @@ public interface ICipherService Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId); Task GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId); Task ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData); - Task<(IEnumerable, Dictionary>)> GetOrganizationCiphers(Guid userId, Guid organizationId); } diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index d6806bd115..196ec6ef3d 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -956,35 +956,6 @@ public class CipherService : ICipherService return restoringCiphers; } - public async Task<(IEnumerable, Dictionary>)> GetOrganizationCiphers(Guid userId, Guid organizationId) - { - if (!await _currentContext.ViewAllCollections(organizationId) && !await _currentContext.AccessReports(organizationId) && !await _currentContext.AccessImportExport(organizationId)) - { - throw new NotFoundException(); - } - - IEnumerable orgCiphers; - if (await _currentContext.AccessImportExport(organizationId)) - { - // Admins, Owners, Providers and Custom (with import/export permission) can access all items even if not assigned to them - orgCiphers = await _cipherRepository.GetManyOrganizationDetailsByOrganizationIdAsync(organizationId); - } - else - { - var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: true); - orgCiphers = ciphers.Where(c => c.OrganizationId == organizationId); - } - - var orgCipherIds = orgCiphers.Select(c => c.Id); - - var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(organizationId); - var collectionCiphersGroupDict = collectionCiphers - .Where(c => orgCipherIds.Contains(c.CipherId)) - .GroupBy(c => c.CipherId).ToDictionary(s => s.Key); - - return (orgCiphers, collectionCiphersGroupDict); - } - private async Task UserCanEditAsync(Cipher cipher, Guid userId) { if (!cipher.OrganizationId.HasValue && cipher.UserId.HasValue && cipher.UserId.Value == userId) diff --git a/test/Core.Test/Services/CollectionServiceTests.cs b/test/Core.Test/Services/CollectionServiceTests.cs index 26e47e83e8..6d788deb05 100644 --- a/test/Core.Test/Services/CollectionServiceTests.cs +++ b/test/Core.Test/Services/CollectionServiceTests.cs @@ -1,5 +1,4 @@ using Bit.Core.AdminConsole.Entities; -using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -176,33 +175,4 @@ public class CollectionServiceTest await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() .LogOrganizationUserEventAsync(default, default); } - - [Theory, BitAutoData] - public async Task GetOrganizationCollectionsAsync_WithViewAllCollectionsTrue_ReturnsAllOrganizationCollections( - Collection collection, Guid organizationId, Guid userId, SutProvider sutProvider) - { - sutProvider.GetDependency().UserId.Returns(userId); - sutProvider.GetDependency() - .GetManyByOrganizationIdAsync(organizationId) - .Returns(new List { collection }); - sutProvider.GetDependency().ViewAllCollections(organizationId).Returns(true); - - var result = await sutProvider.Sut.GetOrganizationCollectionsAsync(organizationId); - - Assert.Single(result); - Assert.Equal(collection, result.First()); - - await sutProvider.GetDependency().Received(1).GetManyByOrganizationIdAsync(organizationId); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByUserIdAsync(default); - } - - [Theory, BitAutoData] - public async Task GetOrganizationCollectionsAsync_WithViewAssignedCollectionsFalse_ThrowsBadRequestException( - Guid organizationId, SutProvider sutProvider) - { - await Assert.ThrowsAsync(() => sutProvider.Sut.GetOrganizationCollectionsAsync(organizationId)); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByOrganizationIdAsync(default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByUserIdAsync(default); - } } From 5423e5d52fe2aac28db457107579aa91709c9bb7 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Fri, 17 Jan 2025 15:58:04 +0100 Subject: [PATCH 029/148] Remove feature flag "browser-fileless-import" (#5282) Co-authored-by: Daniel James Smith --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 656e943dc0..059ca6a1cc 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -101,7 +101,6 @@ public static class AuthenticationSchemes public static class FeatureFlagKeys { - public const string BrowserFilelessImport = "browser-fileless-import"; public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection"; public const string ItemShare = "item-share"; From 04e5626c577f9765b79439cdc09009ab25808173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Mon, 20 Jan 2025 14:59:10 +0000 Subject: [PATCH 030/148] [PM-16777] Fix exception when bulk restoring revoked users who never accepted invitations (#5224) * Fix null handling for UserId in Two Factor Authentication checks * Add tests for restoring users with and without 2FA policies --- .../Implementations/OrganizationService.cs | 6 +- .../Services/OrganizationServiceTests.cs | 103 ++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 56467a661a..b2037644e6 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -2165,7 +2165,8 @@ public class OrganizationService : IOrganizationService // Query Two Factor Authentication status for all users in the organization // This is an optimization to avoid querying the Two Factor Authentication status for each user individually - var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(filteredUsers.Select(ou => ou.UserId.Value)); + var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync( + filteredUsers.Where(ou => ou.UserId.HasValue).Select(ou => ou.UserId.Value)); var result = new List>(); @@ -2188,7 +2189,8 @@ public class OrganizationService : IOrganizationService throw new BadRequestException("Only owners can restore other owners."); } - var twoFactorIsEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value).twoFactorIsEnabled; + var twoFactorIsEnabled = organizationUser.UserId.HasValue + && organizationUsersTwoFactorEnabled.FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value).twoFactorIsEnabled; await CheckPoliciesBeforeRestoreAsync(organizationUser, twoFactorIsEnabled); var status = GetPriorActiveOrganizationUserStatusType(organizationUser); diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index fc839030aa..45cab3912c 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -2180,4 +2180,107 @@ OrganizationUserInvite invite, SutProvider sutProvider) } ); } + + [Theory, BitAutoData] + public async Task RestoreUsers_Success(Organization organization, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2, + SutProvider sutProvider) + { + // Arrange + RestoreRevokeUser_Setup(organization, owner, orgUser1, sutProvider); + var organizationUserRepository = sutProvider.GetDependency(); + var eventService = sutProvider.GetDependency(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); + var userService = Substitute.For(); + + orgUser1.Email = orgUser2.Email = null; // Mock that users were previously confirmed + orgUser1.OrganizationId = orgUser2.OrganizationId = organization.Id; + organizationUserRepository + .GetManyAsync(Arg.Is>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id))) + .Returns(new[] { orgUser1, orgUser2 }); + + twoFactorIsEnabledQuery + .TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)> + { + (orgUser1.UserId!.Value, true), + (orgUser2.UserId!.Value, false) + }); + + // Act + var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, new[] { orgUser1.Id, orgUser2.Id }, owner.Id, userService); + + // Assert + Assert.Equal(2, result.Count); + Assert.All(result, r => Assert.Empty(r.Item2)); // No error messages + await organizationUserRepository + .Received(1) + .RestoreAsync(orgUser1.Id, OrganizationUserStatusType.Confirmed); + await organizationUserRepository + .Received(1) + .RestoreAsync(orgUser2.Id, OrganizationUserStatusType.Confirmed); + await eventService.Received(1) + .LogOrganizationUserEventAsync(orgUser1, EventType.OrganizationUser_Restored); + await eventService.Received(1) + .LogOrganizationUserEventAsync(orgUser2, EventType.OrganizationUser_Restored); + } + + [Theory, BitAutoData] + public async Task RestoreUsers_With2FAPolicy_BlocksNonCompliantUser(Organization organization, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser3, + SutProvider sutProvider) + { + // Arrange + RestoreRevokeUser_Setup(organization, owner, orgUser1, sutProvider); + var organizationUserRepository = sutProvider.GetDependency(); + var userRepository = sutProvider.GetDependency(); + var policyService = sutProvider.GetDependency(); + var userService = Substitute.For(); + + orgUser1.Email = orgUser2.Email = null; + orgUser3.UserId = null; + orgUser3.Key = null; + orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = organization.Id; + organizationUserRepository + .GetManyAsync(Arg.Is>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id))) + .Returns(new[] { orgUser1, orgUser2, orgUser3 }); + + userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = "test@example.com" }); + + // Setup 2FA policy + policyService.GetPoliciesApplicableToUserAsync(Arg.Any(), PolicyType.TwoFactorAuthentication, Arg.Any()) + .Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organization.Id, PolicyType = PolicyType.TwoFactorAuthentication } }); + + // User1 has 2FA, User2 doesn't + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)> + { + (orgUser1.UserId!.Value, true), + (orgUser2.UserId!.Value, false) + }); + + // Act + var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, new[] { orgUser1.Id, orgUser2.Id, orgUser3.Id }, owner.Id, userService); + + // Assert + Assert.Equal(3, result.Count); + Assert.Empty(result[0].Item2); // First user should succeed + Assert.Contains("two-step login", result[1].Item2); // Second user should fail + Assert.Empty(result[2].Item2); // Third user should succeed + await organizationUserRepository + .Received(1) + .RestoreAsync(orgUser1.Id, OrganizationUserStatusType.Confirmed); + await organizationUserRepository + .DidNotReceive() + .RestoreAsync(orgUser2.Id, Arg.Any()); + await organizationUserRepository + .Received(1) + .RestoreAsync(orgUser3.Id, OrganizationUserStatusType.Invited); + } } From ee2d7df061fdcfdae97cbeea46869e0e58497854 Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Mon, 20 Jan 2025 10:49:33 -0500 Subject: [PATCH 031/148] [pm-16949] Include revoked users in applicable policies (#5261) --- .../Services/Implementations/OrganizationService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index b2037644e6..8743e51ff2 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -2249,7 +2249,7 @@ public class OrganizationService : IOrganizationService if (!userHasTwoFactorEnabled) { var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId, - PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); + PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked); if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) { twoFactorCompliant = false; From 0de108e0518e5b47c6b3204c53deb2b82221dfc6 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Mon, 20 Jan 2025 16:50:11 +0100 Subject: [PATCH 032/148] [PM-16682] Fix tax id not being saved for providers (#5257) --- .../Commercial.Core/Billing/ProviderBillingService.cs | 10 ++++------ src/Core/Models/Business/TaxInfo.cs | 6 ------ .../Services/Implementations/StripePaymentService.cs | 2 +- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index 57349042d1..2b834947af 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -352,12 +352,10 @@ public class ProviderBillingService( throw new BadRequestException("billingTaxIdTypeInferenceError"); } - customerCreateOptions.TaxIdData = taxInfo.HasTaxId - ? - [ - new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber } - ] - : null; + customerCreateOptions.TaxIdData = + [ + new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber } + ]; } try diff --git a/src/Core/Models/Business/TaxInfo.cs b/src/Core/Models/Business/TaxInfo.cs index 82a6ddfc3e..80a63473a7 100644 --- a/src/Core/Models/Business/TaxInfo.cs +++ b/src/Core/Models/Business/TaxInfo.cs @@ -11,10 +11,4 @@ public class TaxInfo public string BillingAddressState { get; set; } public string BillingAddressPostalCode { get; set; } public string BillingAddressCountry { get; set; } = "US"; - - public bool HasTaxId - { - get => !string.IsNullOrWhiteSpace(TaxIdNumber) && - !string.IsNullOrWhiteSpace(TaxIdType); - } } diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index e14467f943..3f9c5c53c6 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -167,7 +167,7 @@ public class StripePaymentService : IPaymentService City = taxInfo?.BillingAddressCity, State = taxInfo?.BillingAddressState, }, - TaxIdData = taxInfo.HasTaxId + TaxIdData = !string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber) ? [new CustomerTaxIdDataOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber }] : null }; From 9efcbec041902a64c366e539cbdf3dabc9b62958 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:35:43 -0800 Subject: [PATCH 033/148] [PM-15605] Return VerifyDevices in Profile sync response (#5264) * feat (NewDeviceVerification) : - Database migration scripts for VerifyDevices column in [dbo].[User]. - Updated DeviceValidator to check if user has opted out of device verification. - Added endpoint to AccountsController.cs to allow editing of new User.VerifyDevices property. - Added tests for new methods and endpoint. - Removed Anon attribute from the POST account/verify-devices endpoint. - Updating queries to track dbo.User.VerifyDevices. - Added update to verify email to the new device verification flow. - Updating some tests for CloudOrganizationSignUpCommand that were failing. - Updating ProfileResponseModel to include the new VerifyDevices data to hydrate the state in the web client. --- src/Api/Models/Response/ProfileResponseModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Api/Models/Response/ProfileResponseModel.cs b/src/Api/Models/Response/ProfileResponseModel.cs index a6ed4ebfa2..82ffb05b0b 100644 --- a/src/Api/Models/Response/ProfileResponseModel.cs +++ b/src/Api/Models/Response/ProfileResponseModel.cs @@ -37,6 +37,7 @@ public class ProfileResponseModel : ResponseModel UsesKeyConnector = user.UsesKeyConnector; AvatarColor = user.AvatarColor; CreationDate = user.CreationDate; + VerifyDevices = user.VerifyDevices; Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingUser)); Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p)); ProviderOrganizations = @@ -62,6 +63,7 @@ public class ProfileResponseModel : ResponseModel public bool UsesKeyConnector { get; set; } public string AvatarColor { get; set; } public DateTime CreationDate { get; set; } + public bool VerifyDevices { get; set; } public IEnumerable Organizations { get; set; } public IEnumerable Providers { get; set; } public IEnumerable ProviderOrganizations { get; set; } From edb74add5040c7b848e85170c75692653e2b0518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 21 Jan 2025 10:15:02 +0000 Subject: [PATCH 034/148] [PM-14243] Free organization limit is not enforced when editing user (#5155) * Enforce free organization limit when updating user * Add test for throwing error on accepting admin user joining multiple free organizations * Add test for throwing BadRequest when free organization admin attempts to sign up for another free organization * Fix user ID handling in UpdateOrganizationUserCommand for free organizations * Rename parameter 'user' to 'organizationUser' in UpdateUserAsync method for clarity --- .../IUpdateOrganizationUserCommand.cs | 2 +- .../UpdateOrganizationUserCommand.cs | 52 ++++++++++++------- .../AcceptOrgUserCommandTests.cs | 24 +++++++++ .../UpdateOrganizationUserCommandTests.cs | 31 +++++++++++ .../CloudOrganizationSignUpCommandTests.cs | 23 ++++++++ 5 files changed, 113 insertions(+), 19 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IUpdateOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IUpdateOrganizationUserCommand.cs index c7298e1cd9..0cd5a3295f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IUpdateOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IUpdateOrganizationUserCommand.cs @@ -6,6 +6,6 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interface public interface IUpdateOrganizationUserCommand { - Task UpdateUserAsync(OrganizationUser user, Guid? savingUserId, + Task UpdateUserAsync(OrganizationUser organizationUser, Guid? savingUserId, List? collectionAccess, IEnumerable? groupAccess); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs index c5a4b3da1d..3dd55f9893 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs @@ -1,6 +1,7 @@ #nullable enable using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -49,48 +50,64 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand /// /// Update an organization user. /// - /// The modified user to save. + /// The modified organization user to save. /// The userId of the currently logged in user who is making the change. /// The user's updated collection access. If set to null, this removes all collection access. /// The user's updated group access. If set to null, groups are not updated. /// - public async Task UpdateUserAsync(OrganizationUser user, Guid? savingUserId, + public async Task UpdateUserAsync(OrganizationUser organizationUser, Guid? savingUserId, List? collectionAccess, IEnumerable? groupAccess) { // Avoid multiple enumeration collectionAccess = collectionAccess?.ToList(); groupAccess = groupAccess?.ToList(); - if (user.Id.Equals(default(Guid))) + if (organizationUser.Id.Equals(default(Guid))) { throw new BadRequestException("Invite the user first."); } - var originalUser = await _organizationUserRepository.GetByIdAsync(user.Id); - if (originalUser == null || user.OrganizationId != originalUser.OrganizationId) + var originalOrganizationUser = await _organizationUserRepository.GetByIdAsync(organizationUser.Id); + if (originalOrganizationUser == null || organizationUser.OrganizationId != originalOrganizationUser.OrganizationId) { throw new NotFoundException(); } + var organization = await _organizationRepository.GetByIdAsync(organizationUser.OrganizationId); + if (organization == null) + { + throw new NotFoundException(); + } + + if (organizationUser.UserId.HasValue && organization.PlanType == PlanType.Free && organizationUser.Type is OrganizationUserType.Admin or OrganizationUserType.Owner) + { + // Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this. + var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(organizationUser.UserId.Value); + if (adminCount > 0) + { + throw new BadRequestException("User can only be an admin of one free organization."); + } + } + if (collectionAccess?.Any() == true) { - await ValidateCollectionAccessAsync(originalUser, collectionAccess.ToList()); + await ValidateCollectionAccessAsync(originalOrganizationUser, collectionAccess.ToList()); } if (groupAccess?.Any() == true) { - await ValidateGroupAccessAsync(originalUser, groupAccess.ToList()); + await ValidateGroupAccessAsync(originalOrganizationUser, groupAccess.ToList()); } if (savingUserId.HasValue) { - await _organizationService.ValidateOrganizationUserUpdatePermissions(user.OrganizationId, user.Type, originalUser.Type, user.GetPermissions()); + await _organizationService.ValidateOrganizationUserUpdatePermissions(organizationUser.OrganizationId, organizationUser.Type, originalOrganizationUser.Type, organizationUser.GetPermissions()); } - await _organizationService.ValidateOrganizationCustomPermissionsEnabledAsync(user.OrganizationId, user.Type); + await _organizationService.ValidateOrganizationCustomPermissionsEnabledAsync(organizationUser.OrganizationId, organizationUser.Type); - if (user.Type != OrganizationUserType.Owner && - !await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(user.OrganizationId, new[] { user.Id })) + if (organizationUser.Type != OrganizationUserType.Owner && + !await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId, new[] { organizationUser.Id })) { throw new BadRequestException("Organization must have at least one confirmed owner."); } @@ -106,26 +123,25 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand // Only autoscale (if required) after all validation has passed so that we know it's a valid request before // updating Stripe - if (!originalUser.AccessSecretsManager && user.AccessSecretsManager) + if (!originalOrganizationUser.AccessSecretsManager && organizationUser.AccessSecretsManager) { - var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(user.OrganizationId, 1); + var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organizationUser.OrganizationId, 1); if (additionalSmSeatsRequired > 0) { - var organization = await _organizationRepository.GetByIdAsync(user.OrganizationId); var update = new SecretsManagerSubscriptionUpdate(organization, true) - .AdjustSeats(additionalSmSeatsRequired); + .AdjustSeats(additionalSmSeatsRequired); await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update); } } - await _organizationUserRepository.ReplaceAsync(user, collectionAccess); + await _organizationUserRepository.ReplaceAsync(organizationUser, collectionAccess); if (groupAccess != null) { - await _organizationUserRepository.UpdateGroupsAsync(user.Id, groupAccess); + await _organizationUserRepository.UpdateGroupsAsync(organizationUser.Id, groupAccess); } - await _eventService.LogOrganizationUserEventAsync(user, EventType.OrganizationUser_Updated); + await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Updated); } private async Task ValidateCollectionAccessAsync(OrganizationUser originalUser, diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs index eca4f449b0..2dda23481a 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -182,6 +183,29 @@ public class AcceptOrgUserCommandTests exception.Message); } + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task AcceptOrgUser_AdminOfFreePlanTryingToJoinSecondFreeOrg_ThrowsBadRequest( + OrganizationUserType userType, + SutProvider sutProvider, + User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) + { + // Arrange + SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); + org.PlanType = PlanType.Free; + orgUser.Type = userType; + + sutProvider.GetDependency() + .GetCountByFreeOrganizationAdminUserAsync(user.Id) + .Returns(1); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); + + Assert.Equal("You can only be an admin of one free organization.", exception.Message); + } // AcceptOrgUserByOrgIdAsync tests -------------------------------------------------------------------------------- diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs index 73bf00474b..cd03f9583b 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs @@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -144,6 +145,7 @@ public class UpdateOrganizationUserCommandTests newUserData.Id = oldUserData.Id; newUserData.UserId = oldUserData.UserId; newUserData.OrganizationId = savingUser.OrganizationId = oldUserData.OrganizationId = organization.Id; + newUserData.Type = OrganizationUserType.Admin; newUserData.Permissions = JsonSerializer.Serialize(permissions, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, @@ -159,6 +161,10 @@ public class UpdateOrganizationUserCommandTests .Returns(callInfo => callInfo.Arg>() .Select(guid => new Group { Id = guid, OrganizationId = oldUserData.OrganizationId }).ToList()); + sutProvider.GetDependency() + .GetCountByFreeOrganizationAdminUserAsync(newUserData.Id) + .Returns(0); + await sutProvider.Sut.UpdateUserAsync(newUserData, savingUser.UserId, collections, groups); var organizationService = sutProvider.GetDependency(); @@ -175,6 +181,31 @@ public class UpdateOrganizationUserCommandTests Arg.Is>(i => i.Contains(newUserData.Id))); } + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task UpdateUserAsync_WhenUpdatingUserToAdminOrOwner_WithUserAlreadyAdminOfAnotherFreeOrganization_Throws( + OrganizationUserType userType, + OrganizationUser oldUserData, + OrganizationUser newUserData, + Organization organization, + SutProvider sutProvider) + { + organization.PlanType = PlanType.Free; + newUserData.Type = userType; + + Setup(sutProvider, organization, newUserData, oldUserData); + + sutProvider.GetDependency() + .GetCountByFreeOrganizationAdminUserAsync(newUserData.UserId!.Value) + .Returns(1); + + // Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpdateUserAsync(newUserData, null, null, null)); + Assert.Contains("User can only be an admin of one free organization.", exception.Message); + } + private void Setup(SutProvider sutProvider, Organization organization, OrganizationUser newUser, OrganizationUser oldUser) { diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs index a16b48240c..46b4f0b334 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs @@ -242,4 +242,27 @@ public class CloudICloudOrganizationSignUpCommandTests () => sutProvider.Sut.SignUpOrganizationAsync(signup)); Assert.Contains("You can't subtract Machine Accounts!", exception.Message); } + + [Theory] + [BitAutoData] + public async Task SignUpAsync_Free_ExistingFreeOrgAdmin_ThrowsBadRequest( + SutProvider sutProvider) + { + // Arrange + var signup = new OrganizationSignup + { + Plan = PlanType.Free, + IsFromProvider = false, + Owner = new User { Id = Guid.NewGuid() } + }; + + sutProvider.GetDependency() + .GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id) + .Returns(1); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpOrganizationAsync(signup)); + Assert.Contains("You can only be an admin of one free organization.", exception.Message); + } } From f1893c256c9c71fe5e51aa9ce8257f1d336a83e9 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Tue, 21 Jan 2025 09:53:12 -0500 Subject: [PATCH 035/148] remove feature flag (#5284) Clients PR was merged, now merging server PR. --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 059ca6a1cc..0ebb64ad98 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -153,7 +153,6 @@ 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 const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration"; public const string AuthenticatorSynciOS = "enable-authenticator-sync-ios"; From a9ef4750466fde62ddc1a74a9a5599d9bfb53562 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 10:56:17 -0800 Subject: [PATCH 036/148] [deps]: Update github-action minor (#5296) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 22 +++++++++++----------- .github/workflows/code-references.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/stale-bot.yml | 2 +- .github/workflows/test-database.yml | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 510ce3318b..7d64612aba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -120,7 +120,7 @@ jobs: ls -atlh ../../../ - name: Upload project artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: ${{ matrix.project_name }}.zip path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip @@ -278,7 +278,7 @@ jobs: - name: Build Docker image id: build-docker - uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0 + uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0 with: context: ${{ matrix.base_path }}/${{ matrix.project_name }} file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile @@ -393,7 +393,7 @@ jobs: if: | github.event_name != 'pull_request_target' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: docker-stub-US.zip path: docker-stub-US.zip @@ -403,7 +403,7 @@ jobs: if: | github.event_name != 'pull_request_target' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: docker-stub-EU.zip path: docker-stub-EU.zip @@ -413,7 +413,7 @@ jobs: if: | github.event_name != 'pull_request_target' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: docker-stub-US-sha256.txt path: docker-stub-US-sha256.txt @@ -423,7 +423,7 @@ jobs: if: | github.event_name != 'pull_request_target' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: docker-stub-EU-sha256.txt path: docker-stub-EU-sha256.txt @@ -447,7 +447,7 @@ jobs: GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder" - name: Upload Public API Swagger artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: swagger.json path: swagger.json @@ -481,14 +481,14 @@ jobs: GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder" - name: Upload Internal API Swagger artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: internal.json path: internal.json if-no-files-found: error - name: Upload Identity Swagger artifact - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: identity.json path: identity.json @@ -533,7 +533,7 @@ jobs: - name: Upload project artifact for Windows if: ${{ contains(matrix.target, 'win') == true }} - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: MsSqlMigratorUtility-${{ matrix.target }} path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility.exe @@ -541,7 +541,7 @@ jobs: - name: Upload project artifact if: ${{ contains(matrix.target, 'win') == false }} - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: MsSqlMigratorUtility-${{ matrix.target }} path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility diff --git a/.github/workflows/code-references.yml b/.github/workflows/code-references.yml index 7fcf864866..ce8cb8e467 100644 --- a/.github/workflows/code-references.yml +++ b/.github/workflows/code-references.yml @@ -37,7 +37,7 @@ jobs: - name: Collect id: collect - uses: launchdarkly/find-code-references-in-pull-request@b2d44bb453e13c11fd1a6ada7b1e5f9fb0ace629 # v2.0.1 + uses: launchdarkly/find-code-references-in-pull-request@30f4c4ab2949bbf258b797ced2fbf6dea34df9ce # v2.1.0 with: project-key: default environment-key: dev diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0809ff833f..f749d2e4f0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,7 +85,7 @@ jobs: - name: Create release if: ${{ inputs.release_type != 'Dry Run' }} - uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 + uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0 with: artifacts: "docker-stub-US.zip, docker-stub-US-sha256.txt, diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index f8a25288f2..9420f71cb3 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 + uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 with: stale-issue-label: "needs-reply" stale-pr-label: "needs-changes" diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index 0d6361eca8..b7b06688b4 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -200,7 +200,7 @@ jobs: shell: pwsh - name: Upload DACPAC - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: sql.dacpac path: Sql.dacpac @@ -226,7 +226,7 @@ jobs: shell: pwsh - name: Report validation results - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: report.xml path: | From 7462352e18944ffc7b898108e463994dd4c550f6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:49:20 -0500 Subject: [PATCH 037/148] [deps] DbOps: Update Microsoft.Azure.Cosmos to 3.46.1 (#5290) 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 44b4729a10..ddd9fc26bb 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -42,7 +42,7 @@ - + From 4069ac3a4b728184fc425442af9975ed76e847a3 Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Tue, 21 Jan 2025 15:51:34 -0500 Subject: [PATCH 038/148] Add limit item deletion organization setting migration (#5283) --- .../AdminConsole/Entities/Organization.cs | 6 + .../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 +- .../2025-01-16_01_LimitItemDeletion.sql | 487 +++ ...250116221304_LimitItemDeletion.Designer.cs | 2997 ++++++++++++++++ .../20250116221304_LimitItemDeletion.cs | 28 + .../DatabaseContextModelSnapshot.cs | 3 + ...250116221314_LimitItemDeletion.Designer.cs | 3003 +++++++++++++++++ .../20250116221314_LimitItemDeletion.cs | 28 + .../DatabaseContextModelSnapshot.cs | 3 + ...250116221310_LimitItemDeletion.Designer.cs | 2986 ++++++++++++++++ .../20250116221310_LimitItemDeletion.cs | 28 + .../DatabaseContextModelSnapshot.cs | 3 + 17 files changed, 9589 insertions(+), 8 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-01-16_01_LimitItemDeletion.sql create mode 100644 util/MySqlMigrations/Migrations/20250116221304_LimitItemDeletion.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250116221304_LimitItemDeletion.cs create mode 100644 util/PostgresMigrations/Migrations/20250116221314_LimitItemDeletion.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250116221314_LimitItemDeletion.cs create mode 100644 util/SqliteMigrations/Migrations/20250116221310_LimitItemDeletion.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250116221310_LimitItemDeletion.cs diff --git a/src/Core/AdminConsole/Entities/Organization.cs b/src/Core/AdminConsole/Entities/Organization.cs index 37e21d7f57..54661e22a7 100644 --- a/src/Core/AdminConsole/Entities/Organization.cs +++ b/src/Core/AdminConsole/Entities/Organization.cs @@ -103,6 +103,12 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable, /// public bool AllowAdminAccessToAllCollectionItems { get; set; } + /// + /// If set to true, members can only delete items when they have a Can Manage permission over the collection. + /// If set to false, members can delete items when they have a Can Manage OR Can Edit permission over the collection. + /// + public bool LimitItemDeletion { get; set; } + /// /// Risk Insights is a reporting feature that provides insights into the security of an organization's vault. /// diff --git a/src/Sql/dbo/Stored Procedures/Organization_Create.sql b/src/Sql/dbo/Stored Procedures/Organization_Create.sql index 9f12a3f347..25dfcf893d 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Create.sql @@ -54,7 +54,8 @@ CREATE PROCEDURE [dbo].[Organization_Create] @LimitCollectionCreation BIT = NULL, @LimitCollectionDeletion BIT = NULL, @AllowAdminAccessToAllCollectionItems BIT = 0, - @UseRiskInsights BIT = 0 + @UseRiskInsights BIT = 0, + @LimitItemDeletion BIT = 0 AS BEGIN SET NOCOUNT ON @@ -116,7 +117,8 @@ BEGIN [LimitCollectionCreation], [LimitCollectionDeletion], [AllowAdminAccessToAllCollectionItems], - [UseRiskInsights] + [UseRiskInsights], + [LimitItemDeletion] ) VALUES ( @@ -175,6 +177,7 @@ BEGIN @LimitCollectionCreation, @LimitCollectionDeletion, @AllowAdminAccessToAllCollectionItems, - @UseRiskInsights + @UseRiskInsights, + @LimitItemDeletion ) END diff --git a/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql b/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql index 5959742dae..49ee0f9c1c 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql @@ -24,7 +24,8 @@ BEGIN [LimitCollectionCreation], [LimitCollectionDeletion], [AllowAdminAccessToAllCollectionItems], - [UseRiskInsights] + [UseRiskInsights], + [LimitItemDeletion] FROM [dbo].[Organization] END diff --git a/src/Sql/dbo/Stored Procedures/Organization_Update.sql b/src/Sql/dbo/Stored Procedures/Organization_Update.sql index a1af26851e..6e9fe88f48 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Update.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Update.sql @@ -54,7 +54,8 @@ CREATE PROCEDURE [dbo].[Organization_Update] @LimitCollectionCreation BIT = null, @LimitCollectionDeletion BIT = null, @AllowAdminAccessToAllCollectionItems BIT = 0, - @UseRiskInsights BIT = 0 + @UseRiskInsights BIT = 0, + @LimitItemDeletion BIT = 0 AS BEGIN SET NOCOUNT ON @@ -116,7 +117,8 @@ BEGIN [LimitCollectionCreation] = @LimitCollectionCreation, [LimitCollectionDeletion] = @LimitCollectionDeletion, [AllowAdminAccessToAllCollectionItems] = @AllowAdminAccessToAllCollectionItems, - [UseRiskInsights] = @UseRiskInsights + [UseRiskInsights] = @UseRiskInsights, + [LimitItemDeletion] = @LimitItemDeletion WHERE [Id] = @Id END diff --git a/src/Sql/dbo/Tables/Organization.sql b/src/Sql/dbo/Tables/Organization.sql index 2178494c19..6d10126972 100644 --- a/src/Sql/dbo/Tables/Organization.sql +++ b/src/Sql/dbo/Tables/Organization.sql @@ -53,6 +53,7 @@ CREATE TABLE [dbo].[Organization] ( [SecretsManagerBeta] BIT NOT NULL CONSTRAINT [DF_Organization_SecretsManagerBeta] DEFAULT (0), [LimitCollectionCreation] BIT NOT NULL CONSTRAINT [DF_Organization_LimitCollectionCreation] DEFAULT (0), [LimitCollectionDeletion] BIT NOT NULL CONSTRAINT [DF_Organization_LimitCollectionDeletion] DEFAULT (0), + [LimitItemDeletion] BIT NOT NULL CONSTRAINT [DF_Organization_LimitItemDeletion] DEFAULT (0), [AllowAdminAccessToAllCollectionItems] BIT NOT NULL CONSTRAINT [DF_Organization_AllowAdminAccessToAllCollectionItems] DEFAULT (0), [UseRiskInsights] BIT NOT NULL CONSTRAINT [DF_Organization_UseRiskInsights] DEFAULT (0), CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC) diff --git a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql index fc7ab1d31a..70c7413b75 100644 --- a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql +++ b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql @@ -49,7 +49,8 @@ SELECT O.[LimitCollectionCreation], O.[LimitCollectionDeletion], O.[AllowAdminAccessToAllCollectionItems], - O.[UseRiskInsights] + O.[UseRiskInsights], + O.[LimitItemDeletion] FROM [dbo].[OrganizationUser] OU LEFT JOIN diff --git a/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql b/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql index 4915a406a1..be6b6fdd0e 100644 --- a/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql +++ b/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql @@ -36,7 +36,8 @@ SELECT O.[LimitCollectionDeletion], O.[AllowAdminAccessToAllCollectionItems], O.[UseRiskInsights], - P.[Type] ProviderType + P.[Type] ProviderType, + O.[LimitItemDeletion] FROM [dbo].[ProviderUser] PU INNER JOIN diff --git a/util/Migrator/DbScripts/2025-01-16_01_LimitItemDeletion.sql b/util/Migrator/DbScripts/2025-01-16_01_LimitItemDeletion.sql new file mode 100644 index 0000000000..f207365471 --- /dev/null +++ b/util/Migrator/DbScripts/2025-01-16_01_LimitItemDeletion.sql @@ -0,0 +1,487 @@ + +-- Add Columns +IF COL_LENGTH('[dbo].[Organization]', 'LimitItemDeletion') IS NULL +BEGIN + ALTER TABLE + [dbo].[Organization] + ADD + [LimitItemDeletion] BIT NOT NULL CONSTRAINT [DF_Organization_LimitItemDeletion] DEFAULT (0) +END +GO + + +-- Refresh Views + +CREATE OR ALTER VIEW [dbo].[ProviderUserProviderOrganizationDetailsView] +AS +SELECT + PU.[UserId], + PO.[OrganizationId], + O.[Name], + O.[Enabled], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseScim], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[UseCustomPermissions], + O.[Seats], + O.[MaxCollections], + O.[MaxStorageGb], + O.[Identifier], + PO.[Key], + O.[PublicKey], + O.[PrivateKey], + PU.[Status], + PU.[Type], + PO.[ProviderId], + PU.[Id] ProviderUserId, + P.[Name] ProviderName, + O.[PlanType], + O.[LimitCollectionCreation], + O.[LimitCollectionDeletion], + O.[AllowAdminAccessToAllCollectionItems], + O.[UseRiskInsights], + P.[Type] ProviderType, + O.[LimitItemDeletion] +FROM + [dbo].[ProviderUser] PU +INNER JOIN + [dbo].[ProviderOrganization] PO ON PO.[ProviderId] = PU.[ProviderId] +INNER JOIN + [dbo].[Organization] O ON O.[Id] = PO.[OrganizationId] +INNER JOIN + [dbo].[Provider] P ON P.[Id] = PU.[ProviderId] + +GO + +CREATE OR ALTER VIEW [dbo].[OrganizationUserOrganizationDetailsView] +AS +SELECT + OU.[UserId], + OU.[OrganizationId], + OU.[Id] OrganizationUserId, + O.[Name], + O.[Enabled], + O.[PlanType], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseScim], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[UseCustomPermissions], + O.[UseSecretsManager], + O.[Seats], + O.[MaxCollections], + O.[MaxStorageGb], + O.[Identifier], + OU.[Key], + OU.[ResetPasswordKey], + O.[PublicKey], + O.[PrivateKey], + OU.[Status], + OU.[Type], + SU.[ExternalId] SsoExternalId, + OU.[Permissions], + PO.[ProviderId], + P.[Name] ProviderName, + P.[Type] ProviderType, + SS.[Data] SsoConfig, + OS.[FriendlyName] FamilySponsorshipFriendlyName, + OS.[LastSyncDate] FamilySponsorshipLastSyncDate, + OS.[ToDelete] FamilySponsorshipToDelete, + OS.[ValidUntil] FamilySponsorshipValidUntil, + OU.[AccessSecretsManager], + O.[UsePasswordManager], + O.[SmSeats], + O.[SmServiceAccounts], + O.[LimitCollectionCreation], + O.[LimitCollectionDeletion], + O.[AllowAdminAccessToAllCollectionItems], + O.[UseRiskInsights], + O.[LimitItemDeletion] +FROM + [dbo].[OrganizationUser] OU +LEFT JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] +LEFT JOIN + [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id] +LEFT JOIN + [dbo].[Provider] P ON P.[Id] = PO.[ProviderId] +LEFT JOIN + [dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserID] = OU.[Id] + +GO + + +-- Refresh Stored Procedures + +CREATE OR ALTER PROCEDURE [dbo].[Organization_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @Identifier NVARCHAR(50), + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @Plan NVARCHAR(50), + @PlanType TINYINT, + @Seats INT, + @MaxCollections SMALLINT, + @UsePolicies BIT, + @UseSso BIT, + @UseGroups BIT, + @UseDirectory BIT, + @UseEvents BIT, + @UseTotp BIT, + @Use2fa BIT, + @UseApi BIT, + @UseResetPassword BIT, + @SelfHost BIT, + @UsersGetPremium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @Enabled BIT, + @LicenseKey VARCHAR(100), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), + @TwoFactorProviders NVARCHAR(MAX), + @ExpirationDate DATETIME2(7), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @OwnersNotifiedOfAutoscaling DATETIME2(7), + @MaxAutoscaleSeats INT, + @UseKeyConnector BIT = 0, + @UseScim BIT = 0, + @UseCustomPermissions BIT = 0, + @UseSecretsManager BIT = 0, + @Status TINYINT = 0, + @UsePasswordManager BIT = 1, + @SmSeats INT = null, + @SmServiceAccounts INT = null, + @MaxAutoscaleSmSeats INT= null, + @MaxAutoscaleSmServiceAccounts INT = null, + @SecretsManagerBeta BIT = 0, + @LimitCollectionCreation BIT = NULL, + @LimitCollectionDeletion BIT = NULL, + @AllowAdminAccessToAllCollectionItems BIT = 0, + @UseRiskInsights BIT = 0, + @LimitItemDeletion BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Organization] + ( + [Id], + [Identifier], + [Name], + [BusinessName], + [BusinessAddress1], + [BusinessAddress2], + [BusinessAddress3], + [BusinessCountry], + [BusinessTaxNumber], + [BillingEmail], + [Plan], + [PlanType], + [Seats], + [MaxCollections], + [UsePolicies], + [UseSso], + [UseGroups], + [UseDirectory], + [UseEvents], + [UseTotp], + [Use2fa], + [UseApi], + [UseResetPassword], + [SelfHost], + [UsersGetPremium], + [Storage], + [MaxStorageGb], + [Gateway], + [GatewayCustomerId], + [GatewaySubscriptionId], + [ReferenceData], + [Enabled], + [LicenseKey], + [PublicKey], + [PrivateKey], + [TwoFactorProviders], + [ExpirationDate], + [CreationDate], + [RevisionDate], + [OwnersNotifiedOfAutoscaling], + [MaxAutoscaleSeats], + [UseKeyConnector], + [UseScim], + [UseCustomPermissions], + [UseSecretsManager], + [Status], + [UsePasswordManager], + [SmSeats], + [SmServiceAccounts], + [MaxAutoscaleSmSeats], + [MaxAutoscaleSmServiceAccounts], + [SecretsManagerBeta], + [LimitCollectionCreation], + [LimitCollectionDeletion], + [AllowAdminAccessToAllCollectionItems], + [UseRiskInsights], + [LimitItemDeletion] + ) + VALUES + ( + @Id, + @Identifier, + @Name, + @BusinessName, + @BusinessAddress1, + @BusinessAddress2, + @BusinessAddress3, + @BusinessCountry, + @BusinessTaxNumber, + @BillingEmail, + @Plan, + @PlanType, + @Seats, + @MaxCollections, + @UsePolicies, + @UseSso, + @UseGroups, + @UseDirectory, + @UseEvents, + @UseTotp, + @Use2fa, + @UseApi, + @UseResetPassword, + @SelfHost, + @UsersGetPremium, + @Storage, + @MaxStorageGb, + @Gateway, + @GatewayCustomerId, + @GatewaySubscriptionId, + @ReferenceData, + @Enabled, + @LicenseKey, + @PublicKey, + @PrivateKey, + @TwoFactorProviders, + @ExpirationDate, + @CreationDate, + @RevisionDate, + @OwnersNotifiedOfAutoscaling, + @MaxAutoscaleSeats, + @UseKeyConnector, + @UseScim, + @UseCustomPermissions, + @UseSecretsManager, + @Status, + @UsePasswordManager, + @SmSeats, + @SmServiceAccounts, + @MaxAutoscaleSmSeats, + @MaxAutoscaleSmServiceAccounts, + @SecretsManagerBeta, + @LimitCollectionCreation, + @LimitCollectionDeletion, + @AllowAdminAccessToAllCollectionItems, + @UseRiskInsights, + @LimitItemDeletion + ) +END + +GO + +CREATE OR ALTER PROCEDURE [dbo].[Organization_ReadAbilities] +AS +BEGIN + SET NOCOUNT ON + + SELECT + [Id], + [UseEvents], + [Use2fa], + CASE + WHEN [Use2fa] = 1 AND [TwoFactorProviders] IS NOT NULL AND [TwoFactorProviders] != '{}' THEN + 1 + ELSE + 0 + END AS [Using2fa], + [UsersGetPremium], + [UseCustomPermissions], + [UseSso], + [UseKeyConnector], + [UseScim], + [UseResetPassword], + [UsePolicies], + [Enabled], + [LimitCollectionCreation], + [LimitCollectionDeletion], + [AllowAdminAccessToAllCollectionItems], + [UseRiskInsights], + [LimitItemDeletion] + FROM + [dbo].[Organization] +END + +GO + + + +CREATE OR ALTER PROCEDURE [dbo].[Organization_Update] + @Id UNIQUEIDENTIFIER, + @Identifier NVARCHAR(50), + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @Plan NVARCHAR(50), + @PlanType TINYINT, + @Seats INT, + @MaxCollections SMALLINT, + @UsePolicies BIT, + @UseSso BIT, + @UseGroups BIT, + @UseDirectory BIT, + @UseEvents BIT, + @UseTotp BIT, + @Use2fa BIT, + @UseApi BIT, + @UseResetPassword BIT, + @SelfHost BIT, + @UsersGetPremium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @Enabled BIT, + @LicenseKey VARCHAR(100), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), + @TwoFactorProviders NVARCHAR(MAX), + @ExpirationDate DATETIME2(7), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @OwnersNotifiedOfAutoscaling DATETIME2(7), + @MaxAutoscaleSeats INT, + @UseKeyConnector BIT = 0, + @UseScim BIT = 0, + @UseCustomPermissions BIT = 0, + @UseSecretsManager BIT = 0, + @Status TINYINT = 0, + @UsePasswordManager BIT = 1, + @SmSeats INT = null, + @SmServiceAccounts INT = null, + @MaxAutoscaleSmSeats INT = null, + @MaxAutoscaleSmServiceAccounts INT = null, + @SecretsManagerBeta BIT = 0, + @LimitCollectionCreation BIT = null, + @LimitCollectionDeletion BIT = null, + @AllowAdminAccessToAllCollectionItems BIT = 0, + @UseRiskInsights BIT = 0, + @LimitItemDeletion BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Organization] + SET + [Identifier] = @Identifier, + [Name] = @Name, + [BusinessName] = @BusinessName, + [BusinessAddress1] = @BusinessAddress1, + [BusinessAddress2] = @BusinessAddress2, + [BusinessAddress3] = @BusinessAddress3, + [BusinessCountry] = @BusinessCountry, + [BusinessTaxNumber] = @BusinessTaxNumber, + [BillingEmail] = @BillingEmail, + [Plan] = @Plan, + [PlanType] = @PlanType, + [Seats] = @Seats, + [MaxCollections] = @MaxCollections, + [UsePolicies] = @UsePolicies, + [UseSso] = @UseSso, + [UseGroups] = @UseGroups, + [UseDirectory] = @UseDirectory, + [UseEvents] = @UseEvents, + [UseTotp] = @UseTotp, + [Use2fa] = @Use2fa, + [UseApi] = @UseApi, + [UseResetPassword] = @UseResetPassword, + [SelfHost] = @SelfHost, + [UsersGetPremium] = @UsersGetPremium, + [Storage] = @Storage, + [MaxStorageGb] = @MaxStorageGb, + [Gateway] = @Gateway, + [GatewayCustomerId] = @GatewayCustomerId, + [GatewaySubscriptionId] = @GatewaySubscriptionId, + [ReferenceData] = @ReferenceData, + [Enabled] = @Enabled, + [LicenseKey] = @LicenseKey, + [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey, + [TwoFactorProviders] = @TwoFactorProviders, + [ExpirationDate] = @ExpirationDate, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [OwnersNotifiedOfAutoscaling] = @OwnersNotifiedOfAutoscaling, + [MaxAutoscaleSeats] = @MaxAutoscaleSeats, + [UseKeyConnector] = @UseKeyConnector, + [UseScim] = @UseScim, + [UseCustomPermissions] = @UseCustomPermissions, + [UseSecretsManager] = @UseSecretsManager, + [Status] = @Status, + [UsePasswordManager] = @UsePasswordManager, + [SmSeats] = @SmSeats, + [SmServiceAccounts] = @SmServiceAccounts, + [MaxAutoscaleSmSeats] = @MaxAutoscaleSmSeats, + [MaxAutoscaleSmServiceAccounts] = @MaxAutoscaleSmServiceAccounts, + [SecretsManagerBeta] = @SecretsManagerBeta, + [LimitCollectionCreation] = @LimitCollectionCreation, + [LimitCollectionDeletion] = @LimitCollectionDeletion, + [AllowAdminAccessToAllCollectionItems] = @AllowAdminAccessToAllCollectionItems, + [UseRiskInsights] = @UseRiskInsights, + [LimitItemDeletion] = @LimitItemDeletion + WHERE + [Id] = @Id +END + + +GO diff --git a/util/MySqlMigrations/Migrations/20250116221304_LimitItemDeletion.Designer.cs b/util/MySqlMigrations/Migrations/20250116221304_LimitItemDeletion.Designer.cs new file mode 100644 index 0000000000..19dbdcdead --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250116221304_LimitItemDeletion.Designer.cs @@ -0,0 +1,2997 @@ +// +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("20250116221304_LimitItemDeletion")] + partial class LimitItemDeletion + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitItemDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseRiskInsights") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("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.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("VerifyDevices") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .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.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.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/20250116221304_LimitItemDeletion.cs b/util/MySqlMigrations/Migrations/20250116221304_LimitItemDeletion.cs new file mode 100644 index 0000000000..19aa5a55a9 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250116221304_LimitItemDeletion.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class LimitItemDeletion : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LimitItemDeletion", + table: "Organization", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LimitItemDeletion", + table: "Organization"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 46d2a2e5fd..5761cd559f 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -94,6 +94,9 @@ namespace Bit.MySqlMigrations.Migrations b.Property("LimitCollectionDeletion") .HasColumnType("tinyint(1)"); + b.Property("LimitItemDeletion") + .HasColumnType("tinyint(1)"); + b.Property("MaxAutoscaleSeats") .HasColumnType("int"); diff --git a/util/PostgresMigrations/Migrations/20250116221314_LimitItemDeletion.Designer.cs b/util/PostgresMigrations/Migrations/20250116221314_LimitItemDeletion.Designer.cs new file mode 100644 index 0000000000..90799c7699 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250116221314_LimitItemDeletion.Designer.cs @@ -0,0 +1,3003 @@ +// +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("20250116221314_LimitItemDeletion")] + partial class LimitItemDeletion + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("LimitItemDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseRiskInsights") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("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.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property("VerifyDevices") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .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.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.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/20250116221314_LimitItemDeletion.cs b/util/PostgresMigrations/Migrations/20250116221314_LimitItemDeletion.cs new file mode 100644 index 0000000000..380ecf507e --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250116221314_LimitItemDeletion.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class LimitItemDeletion : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LimitItemDeletion", + table: "Organization", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LimitItemDeletion", + table: "Organization"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 29672b80a9..4abfec0343 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -96,6 +96,9 @@ namespace Bit.PostgresMigrations.Migrations b.Property("LimitCollectionDeletion") .HasColumnType("boolean"); + b.Property("LimitItemDeletion") + .HasColumnType("boolean"); + b.Property("MaxAutoscaleSeats") .HasColumnType("integer"); diff --git a/util/SqliteMigrations/Migrations/20250116221310_LimitItemDeletion.Designer.cs b/util/SqliteMigrations/Migrations/20250116221310_LimitItemDeletion.Designer.cs new file mode 100644 index 0000000000..91015f9300 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250116221310_LimitItemDeletion.Designer.cs @@ -0,0 +1,2986 @@ +// +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("20250116221310_LimitItemDeletion")] + partial class LimitItemDeletion + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitItemDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseRiskInsights") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("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.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("VerifyDevices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .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.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.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/20250116221310_LimitItemDeletion.cs b/util/SqliteMigrations/Migrations/20250116221310_LimitItemDeletion.cs new file mode 100644 index 0000000000..ded7357312 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250116221310_LimitItemDeletion.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class LimitItemDeletion : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LimitItemDeletion", + table: "Organization", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LimitItemDeletion", + table: "Organization"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index d71327ec3c..f90de08a93 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -89,6 +89,9 @@ namespace Bit.SqliteMigrations.Migrations b.Property("LimitCollectionDeletion") .HasColumnType("INTEGER"); + b.Property("LimitItemDeletion") + .HasColumnType("INTEGER"); + b.Property("MaxAutoscaleSeats") .HasColumnType("INTEGER"); From 163a74000d63ff6276a1ad294ce2fba3242e64c1 Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Tue, 21 Jan 2025 16:32:30 -0500 Subject: [PATCH 039/148] Add Authenticator sync flags (#5307) --- src/Core/Constants.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 0ebb64ad98..659a377d59 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -161,6 +161,8 @@ public static class FeatureFlagKeys public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs"; public const string UsePricingService = "use-pricing-service"; public const string RecordInstallationLastActivityDate = "installation-last-activity-date"; + public const string EnablePasswordManagerSyncAndroid = "enable-password-manager-sync-android"; + public const string EnablePasswordManagerSynciOS = "enable-password-manager-sync-ios"; public static List GetAllKeys() { From c67181830450e89a5f041116de182bae2ae541b9 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 22 Jan 2025 14:14:59 +0100 Subject: [PATCH 040/148] Add argon2-default flag (#5253) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 659a377d59..5b1e9175cf 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -159,6 +159,7 @@ public static class FeatureFlagKeys public const string AuthenticatorSyncAndroid = "enable-authenticator-sync-android"; public const string AppReviewPrompt = "app-review-prompt"; public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs"; + public const string Argon2Default = "argon2-default"; public const string UsePricingService = "use-pricing-service"; public const string RecordInstallationLastActivityDate = "installation-last-activity-date"; public const string EnablePasswordManagerSyncAndroid = "enable-password-manager-sync-android"; From cb76cdb5d3b3391e6ba1ca52c8baccfbd8c0f3f6 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 23 Jan 2025 00:04:08 +1000 Subject: [PATCH 041/148] Group AC Team feature flags (#5309) --- src/Core/Constants.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 5b1e9175cf..dd45593ae9 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -101,6 +101,12 @@ public static class AuthenticationSchemes public static class FeatureFlagKeys { + /* Admin Console Team */ + public const string ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner"; + public const string AccountDeprovisioning = "pm-10308-account-deprovisioning"; + public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; + public const string IntegrationPage = "pm-14505-admin-console-integration-page"; + public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection"; public const string ItemShare = "item-share"; @@ -117,7 +123,6 @@ public static class FeatureFlagKeys public const string InlineMenuFieldQualification = "inline-menu-field-qualification"; public const string TwoFactorComponentRefactor = "two-factor-component-refactor"; public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements"; - public const string ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner"; public const string DeviceTrustLogging = "pm-8285-device-trust-logging"; public const string SSHKeyItemVaultItem = "ssh-key-vault-item"; public const string SSHAgent = "ssh-agent"; @@ -129,7 +134,6 @@ public static class FeatureFlagKeys public const string DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2"; public const string NativeCarouselFlow = "native-carousel-flow"; public const string NativeCreateAccountFlow = "native-create-account-flow"; - public const string AccountDeprovisioning = "pm-10308-account-deprovisioning"; public const string NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements"; public const string BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain"; public const string NotificationRefresh = "notification-refresh"; @@ -140,12 +144,10 @@ public static class FeatureFlagKeys public const string StorageReseedRefactor = "storage-reseed-refactor"; public const string TrialPayment = "PM-8163-trial-payment"; public const string RemoveServerVersionHeader = "remove-server-version-header"; - public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises"; public const string GeneratorToolsModernization = "generator-tools-modernization"; public const string NewDeviceVerification = "new-device-verification"; public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; - public const string IntegrationPage = "pm-14505-admin-console-integration-page"; public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss"; public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss"; public const string SecurityTasks = "security-tasks"; From 0e0dd8203a40b1e54ae9ea1ae894e0d9cbdadc3a Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Wed, 22 Jan 2025 11:41:18 -0500 Subject: [PATCH 042/148] [PM-14818] Update `migrate.ps1` to support test database used by integration tests (#4912) * Check for correct database in an old MySql migration * Update `migrate.ps1` to support integration test databases --- dev/migrate.ps1 | 74 +++++++++++++------ .../20231214162533_GrantIdWithIndexes.cs | 6 +- 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/dev/migrate.ps1 b/dev/migrate.ps1 index ee78e90d32..d129af4e6e 100755 --- a/dev/migrate.ps1 +++ b/dev/migrate.ps1 @@ -7,11 +7,13 @@ param( [switch]$mysql, [switch]$mssql, [switch]$sqlite, - [switch]$selfhost + [switch]$selfhost, + [switch]$test ) # Abort on any error $ErrorActionPreference = "Stop" +$currentDir = Get-Location if (!$all -and !$postgres -and !$mysql -and !$sqlite) { $mssql = $true; @@ -25,36 +27,62 @@ if ($all -or $postgres -or $mysql -or $sqlite) { } } -if ($all -or $mssql) { - function Get-UserSecrets { - # The dotnet cli command sometimes adds //BEGIN and //END comments to the output, Where-Object removes comments - # to ensure a valid json - return dotnet user-secrets list --json --project ../src/Api | Where-Object { $_ -notmatch "^//" } | ConvertFrom-Json - } - - if ($selfhost) { - $msSqlConnectionString = $(Get-UserSecrets).'dev:selfHostOverride:globalSettings:sqlServer:connectionString' - $envName = "self-host" - } else { - $msSqlConnectionString = $(Get-UserSecrets).'globalSettings:sqlServer:connectionString' - $envName = "cloud" - } - - Write-Host "Starting Microsoft SQL Server Migrations for $envName" - - dotnet run --project ../util/MsSqlMigratorUtility/ "$msSqlConnectionString" +function Get-UserSecrets { + # The dotnet cli command sometimes adds //BEGIN and //END comments to the output, Where-Object removes comments + # to ensure a valid json + return dotnet user-secrets list --json --project "$currentDir/../src/Api" | Where-Object { $_ -notmatch "^//" } | ConvertFrom-Json } -$currentDir = Get-Location +if ($all -or $mssql) { + if ($all -or !$test) { + if ($selfhost) { + $msSqlConnectionString = $(Get-UserSecrets).'dev:selfHostOverride:globalSettings:sqlServer:connectionString' + $envName = "self-host" + } else { + $msSqlConnectionString = $(Get-UserSecrets).'globalSettings:sqlServer:connectionString' + $envName = "cloud" + } -Foreach ($item in @(@($mysql, "MySQL", "MySqlMigrations"), @($postgres, "PostgreSQL", "PostgresMigrations"), @($sqlite, "SQLite", "SqliteMigrations"))) { + Write-Host "Starting Microsoft SQL Server Migrations for $envName" + dotnet run --project ../util/MsSqlMigratorUtility/ "$msSqlConnectionString" + } + + if ($all -or $test) { + $testMsSqlConnectionString = $(Get-UserSecrets).'databases:3:connectionString' + if ($testMsSqlConnectionString) { + $testEnvName = "test databases" + Write-Host "Starting Microsoft SQL Server Migrations for $testEnvName" + dotnet run --project ../util/MsSqlMigratorUtility/ "$testMsSqlConnectionString" + } else { + Write-Host "Connection string for a test MSSQL database not found in secrets.json!" + } + } +} + +Foreach ($item in @( + @($mysql, "MySQL", "MySqlMigrations", "mySql", 2), + @($postgres, "PostgreSQL", "PostgresMigrations", "postgreSql", 0), + @($sqlite, "SQLite", "SqliteMigrations", "sqlite", 1) +)) { if (!$item[0] -and !$all) { continue } - Write-Host "Starting $($item[1]) Migrations" Set-Location "$currentDir/../util/$($item[2])/" - dotnet ef database update + if(!$test -or $all) { + Write-Host "Starting $($item[1]) Migrations" + $connectionString = $(Get-UserSecrets)."globalSettings:$($item[3]):connectionString" + dotnet ef database update --connection "$connectionString" + } + if ($test -or $all) { + $testConnectionString = $(Get-UserSecrets)."databases:$($item[4]):connectionString" + if ($testConnectionString) { + Write-Host "Starting $($item[1]) Migrations for test databases" + dotnet ef database update --connection "$testConnectionString" + } else { + Write-Host "Connection string for a test $($item[1]) database not found in secrets.json!" + } + } } Set-Location "$currentDir" diff --git a/util/MySqlMigrations/Migrations/20231214162533_GrantIdWithIndexes.cs b/util/MySqlMigrations/Migrations/20231214162533_GrantIdWithIndexes.cs index 1e4c178ade..e65a4dc6bf 100644 --- a/util/MySqlMigrations/Migrations/20231214162533_GrantIdWithIndexes.cs +++ b/util/MySqlMigrations/Migrations/20231214162533_GrantIdWithIndexes.cs @@ -74,13 +74,13 @@ public partial class GrantIdWithIndexes : Migration migrationBuilder.Sql(@" DROP PROCEDURE IF EXISTS GrantSchemaChange; - + CREATE PROCEDURE GrantSchemaChange() BEGIN - IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Grant' AND COLUMN_NAME = 'Id') THEN + IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Grant' AND COLUMN_NAME = 'Id' AND TABLE_SCHEMA=database()) THEN ALTER TABLE `Grant` DROP COLUMN `Id`; END IF; - + ALTER TABLE `Grant` ADD COLUMN `Id` INT AUTO_INCREMENT UNIQUE; END; From 28a592103d263b4b3770da36e5ba635d4748214e Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:26:21 -0500 Subject: [PATCH 043/148] Updated invoice history to filter on customerId only (#5175) --- .../Billing/Services/Implementations/PaymentHistoryService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs b/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs index 69e1a4cfba..6e984f946e 100644 --- a/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs +++ b/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs @@ -28,7 +28,6 @@ public class PaymentHistoryService( var invoices = await stripeAdapter.InvoiceListAsync(new StripeInvoiceListOptions { Customer = subscriber.GatewayCustomerId, - Subscription = subscriber.GatewaySubscriptionId, Limit = pageSize, Status = status, StartingAfter = startAfter From 8f8a599c07742dd34a400a1afc591b283d12cd2a Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Wed, 22 Jan 2025 13:09:46 -0500 Subject: [PATCH 044/148] Use .db extension for SQLite configuration example (#5313) --- dev/secrets.json.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/secrets.json.example b/dev/secrets.json.example index e296ffb7c0..7c91669b39 100644 --- a/dev/secrets.json.example +++ b/dev/secrets.json.example @@ -21,7 +21,7 @@ "connectionString": "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev" }, "sqlite": { - "connectionString": "Data Source=/path/to/bitwardenServer/repository/server/dev/db/bitwarden.sqlite" + "connectionString": "Data Source=/path/to/bitwardenServer/repository/server/dev/db/bitwarden.db" }, "identityServer": { "certificateThumbprint": "" From 9e7d1abdf12e411d94f38ab404e5c6faab07e6da Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 22 Jan 2025 19:27:11 +0100 Subject: [PATCH 045/148] changes for update to current plan (#5312) --- src/Api/Billing/Controllers/OrganizationBillingController.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 1c0cfd9388..4a6f5f5b8a 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -310,6 +310,9 @@ public class OrganizationBillingController( } var organizationSignup = model.ToOrganizationSignup(user); var sale = OrganizationSale.From(organization, organizationSignup); + var plan = StaticStore.GetPlan(model.PlanType); + sale.Organization.PlanType = plan.Type; + sale.Organization.Plan = plan.Name; await organizationBillingService.Finalize(sale); return TypedResults.Ok(); From e8cd86e5f6da1f9e0c917fa7f30f5abc3738048a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 12:51:57 +0100 Subject: [PATCH 046/148] [deps] Billing: Update xunit.runner.visualstudio to v3 (#5183) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@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 82d63bd3c1..82a92989d1 100644 --- a/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj +++ b/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj @@ -12,7 +12,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 31e95d529f54a8dbc7f90412a893cd08ac1dbd3e Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Thu, 23 Jan 2025 10:00:51 -0500 Subject: [PATCH 047/148] Added some defensive logging around making braintree payments (#5317) --- .../StripeEventUtilityService.cs | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/src/Billing/Services/Implementations/StripeEventUtilityService.cs b/src/Billing/Services/Implementations/StripeEventUtilityService.cs index 520205e745..8077ec5b57 100644 --- a/src/Billing/Services/Implementations/StripeEventUtilityService.cs +++ b/src/Billing/Services/Implementations/StripeEventUtilityService.cs @@ -318,26 +318,34 @@ public class StripeEventUtilityService : IStripeEventUtilityService Result transactionResult; try { - transactionResult = await _btGateway.Transaction.SaleAsync( - new Braintree.TransactionRequest + var transactionRequest = new Braintree.TransactionRequest + { + Amount = btInvoiceAmount, + CustomerId = customer.Metadata["btCustomerId"], + Options = new Braintree.TransactionOptionsRequest { - Amount = btInvoiceAmount, - CustomerId = customer.Metadata["btCustomerId"], - Options = new Braintree.TransactionOptionsRequest + SubmitForSettlement = true, + PayPal = new Braintree.TransactionOptionsPayPalRequest { - SubmitForSettlement = true, - PayPal = new Braintree.TransactionOptionsPayPalRequest - { - CustomField = - $"{btObjIdField}:{btObjId},region:{_globalSettings.BaseServiceUri.CloudRegion}" - } - }, - CustomFields = new Dictionary - { - [btObjIdField] = btObjId.ToString(), - ["region"] = _globalSettings.BaseServiceUri.CloudRegion + CustomField = + $"{btObjIdField}:{btObjId},region:{_globalSettings.BaseServiceUri.CloudRegion}" } - }); + }, + CustomFields = new Dictionary + { + [btObjIdField] = btObjId.ToString(), + ["region"] = _globalSettings.BaseServiceUri.CloudRegion + } + }; + + _logger.LogInformation("Creating Braintree transaction with Amount: {Amount}, CustomerId: {CustomerId}, " + + "CustomField: {CustomField}, CustomFields: {@CustomFields}", + transactionRequest.Amount, + transactionRequest.CustomerId, + transactionRequest.Options.PayPal.CustomField, + transactionRequest.CustomFields); + + transactionResult = await _btGateway.Transaction.SaleAsync(transactionRequest); } catch (NotFoundException e) { @@ -345,9 +353,19 @@ public class StripeEventUtilityService : IStripeEventUtilityService "Attempted to make a payment with Braintree, but customer did not exist for the given btCustomerId present on the Stripe metadata"); throw; } + catch (Exception e) + { + _logger.LogError(e, "Exception occurred while trying to pay invoice with Braintree"); + throw; + } if (!transactionResult.IsSuccess()) { + _logger.LogWarning("Braintree transaction failed. Error: {ErrorMessage}, Transaction Status: {Status}, Validation Errors: {ValidationErrors}", + transactionResult.Message, + transactionResult.Target?.Status, + string.Join(", ", transactionResult.Errors.DeepAll().Select(e => $"Code: {e.Code}, Message: {e.Message}, Attribute: {e.Attribute}"))); + if (invoice.AttemptCount < 4) { await _mailService.SendPaymentFailedAsync(customer.Email, btInvoiceAmount, true); From 20fb45b05c95dcde7d957190f76e98cc3efffc18 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:19:46 -0500 Subject: [PATCH 048/148] Round PayPal transaction amount to two decimal points (#5318) --- .../Services/Implementations/StripeEventUtilityService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Billing/Services/Implementations/StripeEventUtilityService.cs b/src/Billing/Services/Implementations/StripeEventUtilityService.cs index 8077ec5b57..48e81dee61 100644 --- a/src/Billing/Services/Implementations/StripeEventUtilityService.cs +++ b/src/Billing/Services/Implementations/StripeEventUtilityService.cs @@ -296,7 +296,7 @@ public class StripeEventUtilityService : IStripeEventUtilityService btObjIdField = "provider_id"; btObjId = providerId.Value; } - var btInvoiceAmount = invoice.AmountDue / 100M; + var btInvoiceAmount = Math.Round(invoice.AmountDue / 100M, 2); var existingTransactions = organizationId.HasValue ? await _transactionRepository.GetManyByOrganizationIdAsync(organizationId.Value) @@ -338,7 +338,7 @@ public class StripeEventUtilityService : IStripeEventUtilityService } }; - _logger.LogInformation("Creating Braintree transaction with Amount: {Amount}, CustomerId: {CustomerId}, " + + _logger.LogInformation("Creating Braintree transaction with Amount: {Amount}, CustomerId: {CustomerId}, " + "CustomField: {CustomField}, CustomFields: {@CustomFields}", transactionRequest.Amount, transactionRequest.CustomerId, From 275f7ceb27ad0c1572844b06cba860dd3f3d7fd8 Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Thu, 23 Jan 2025 11:21:28 -0500 Subject: [PATCH 049/148] Auth/pm 17233/tests for multiple users on single device for web approvals (#5316) * test(test-device-repository): [PM-17233] Add Test Case for Critical Bug Found in Device Repository - Added new test case for previously found bug. --- .../Repositories/DeviceRepositoryTests.cs | 67 ++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/test/Infrastructure.IntegrationTest/Auth/Repositories/DeviceRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Auth/Repositories/DeviceRepositoryTests.cs index a9eec23194..95b88d5662 100644 --- a/test/Infrastructure.IntegrationTest/Auth/Repositories/DeviceRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Auth/Repositories/DeviceRepositoryTests.cs @@ -73,6 +73,71 @@ public class DeviceRepositoryTests Assert.Equal(response.First().AuthRequestId, freshAuthRequest.Id); } + [DatabaseTheory] + [DatabaseData] + public async Task GetManyByUserIdWithDeviceAuth_WorksWithMultipleUsersOnSameDevice_ReturnsExpectedResults( + IDeviceRepository sutRepository, + IUserRepository userRepository, + IAuthRequestRepository authRequestRepository) + { + // Arrange + var userA = await userRepository.CreateAsync(new User + { + Name = "Test User A", + Email = $"test_user_A+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var userB = await userRepository.CreateAsync(new User + { + Name = "Test User B", + Email = $"test_user_B+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var sharedDeviceIdentifier = Guid.NewGuid().ToString(); + + var deviceForUserA = await sutRepository.CreateAsync(new Device + { + Active = true, + Name = "chrome-test", + UserId = userA.Id, + Type = DeviceType.ChromeBrowser, + Identifier = sharedDeviceIdentifier, + }); + + var deviceForUserB = await sutRepository.CreateAsync(new Device + { + Active = true, + Name = "chrome-test", + UserId = userB.Id, + Type = DeviceType.ChromeBrowser, + Identifier = sharedDeviceIdentifier, + }); + + var userAAuthRequest = await authRequestRepository.CreateAsync(new AuthRequest + { + ResponseDeviceId = null, + Approved = null, + Type = AuthRequestType.AuthenticateAndUnlock, + OrganizationId = null, + UserId = userA.Id, + RequestIpAddress = ":1", + RequestDeviceIdentifier = deviceForUserA.Identifier, + AccessCode = "AccessCode_1234", + PublicKey = "PublicKey_1234" + }); + + // Act + var response = await sutRepository.GetManyByUserIdWithDeviceAuth(userB.Id); + + // Assert + Assert.Null(response.First().AuthRequestId); + Assert.Null(response.First().AuthRequestCreatedAt); + } + [DatabaseTheory] [DatabaseData] public async Task GetManyByUserIdWithDeviceAuth_WorksWithNoAuthRequestAndMultipleDevices_ReturnsExpectedResults( @@ -117,7 +182,7 @@ public class DeviceRepositoryTests [DatabaseTheory] [DatabaseData] - public async Task GetManyByUserIdWithDeviceAuth_FailsToRespondWithAnyAuthData_ReturnsExpectedResults( + public async Task GetManyByUserIdWithDeviceAuth_FailsToRespondWithAnyAuthData_ReturnsEmptyResults( IDeviceRepository sutRepository, IUserRepository userRepository, IAuthRequestRepository authRequestRepository) From ca217584927ee00a5a04cfc61b9523f04a8f281b Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Thu, 23 Jan 2025 08:23:45 -0800 Subject: [PATCH 050/148] feat (newDeviceVerification) Added conditional for selfhosted to manage access to feature --- src/Admin/Views/Users/Edit.cshtml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Admin/Views/Users/Edit.cshtml b/src/Admin/Views/Users/Edit.cshtml index 495fc43c2f..04e95c1400 100644 --- a/src/Admin/Views/Users/Edit.cshtml +++ b/src/Admin/Views/Users/Edit.cshtml @@ -1,6 +1,7 @@ @using Bit.Admin.Enums; @inject Bit.Admin.Services.IAccessControlService AccessControlService @inject Bit.Core.Services.IFeatureService FeatureService +@inject Bit.Core.Settings.GlobalSettings GlobalSettings @inject IWebHostEnvironment HostingEnvironment @model UserEditModel @{ @@ -8,6 +9,7 @@ var canViewUserInformation = AccessControlService.UserHasPermission(Permission.User_UserInformation_View); var canViewNewDeviceException = AccessControlService.UserHasPermission(Permission.User_NewDeviceException_Edit) && + GlobalSettings.EnableNewDeviceVerification && FeatureService.IsEnabled(Bit.Core.FeatureFlagKeys.NewDeviceVerification); var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.User_BillingInformation_View); var canViewGeneral = AccessControlService.UserHasPermission(Permission.User_GeneralDetails_View); From ef32e807258cd1aa150f78c0ae3486f53dc2c00a Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Fri, 24 Jan 2025 12:02:13 +0100 Subject: [PATCH 051/148] [PM-15807]Move subscription to 'canceled' 7 days after unpaid (#5221) * Changes to implement the cancel job Signed-off-by: Cy Okeke * Resolve the Dependency issues Signed-off-by: Cy Okeke * changes when open invoices is more than 10 Signed-off-by: Cy Okeke * Move the package reference to ore Signed-off-by: Cy Okeke --------- Signed-off-by: Cy Okeke --- src/Billing/Jobs/JobsHostedService.cs | 1 + .../Jobs/SubscriptionCancellationJob.cs | 58 +++++++++++++++++++ .../SubscriptionUpdatedHandler.cs | 38 +++++++++++- src/Billing/Startup.cs | 8 +++ src/Core/Core.csproj | 3 + 5 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/Billing/Jobs/SubscriptionCancellationJob.cs diff --git a/src/Billing/Jobs/JobsHostedService.cs b/src/Billing/Jobs/JobsHostedService.cs index d91ca21520..a6e702c662 100644 --- a/src/Billing/Jobs/JobsHostedService.cs +++ b/src/Billing/Jobs/JobsHostedService.cs @@ -32,5 +32,6 @@ public class JobsHostedService : BaseJobsHostedService public static void AddJobsServices(IServiceCollection services) { services.AddTransient(); + services.AddTransient(); } } diff --git a/src/Billing/Jobs/SubscriptionCancellationJob.cs b/src/Billing/Jobs/SubscriptionCancellationJob.cs new file mode 100644 index 0000000000..c46581272e --- /dev/null +++ b/src/Billing/Jobs/SubscriptionCancellationJob.cs @@ -0,0 +1,58 @@ +using Bit.Billing.Services; +using Bit.Core.Repositories; +using Quartz; +using Stripe; + +namespace Bit.Billing.Jobs; + +public class SubscriptionCancellationJob( + IStripeFacade stripeFacade, + IOrganizationRepository organizationRepository) + : IJob +{ + public async Task Execute(IJobExecutionContext context) + { + var subscriptionId = context.MergedJobDataMap.GetString("subscriptionId"); + var organizationId = new Guid(context.MergedJobDataMap.GetString("organizationId") ?? string.Empty); + + var organization = await organizationRepository.GetByIdAsync(organizationId); + if (organization == null || organization.Enabled) + { + // Organization was deleted or re-enabled by CS, skip cancellation + return; + } + + var subscription = await stripeFacade.GetSubscription(subscriptionId); + if (subscription?.Status != "unpaid") + { + // Subscription is no longer unpaid, skip cancellation + return; + } + + // Cancel the subscription + await stripeFacade.CancelSubscription(subscriptionId, new SubscriptionCancelOptions()); + + // Void any open invoices + var options = new InvoiceListOptions + { + Status = "open", + Subscription = subscriptionId, + Limit = 100 + }; + var invoices = await stripeFacade.ListInvoices(options); + foreach (var invoice in invoices) + { + await stripeFacade.VoidInvoice(invoice.Id); + } + + while (invoices.HasMore) + { + options.StartingAfter = invoices.Data.Last().Id; + invoices = await stripeFacade.ListInvoices(options); + foreach (var invoice in invoices) + { + await stripeFacade.VoidInvoice(invoice.Id); + } + } + } +} diff --git a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs index 6b4fef43d1..ea277a6307 100644 --- a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs @@ -1,9 +1,12 @@ using Bit.Billing.Constants; +using Bit.Billing.Jobs; +using Bit.Core; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; +using Quartz; using Stripe; using Event = Stripe.Event; @@ -19,6 +22,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler private readonly IUserService _userService; private readonly IPushNotificationService _pushNotificationService; private readonly IOrganizationRepository _organizationRepository; + private readonly ISchedulerFactory _schedulerFactory; + private readonly IFeatureService _featureService; public SubscriptionUpdatedHandler( IStripeEventService stripeEventService, @@ -28,7 +33,9 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler IOrganizationSponsorshipRenewCommand organizationSponsorshipRenewCommand, IUserService userService, IPushNotificationService pushNotificationService, - IOrganizationRepository organizationRepository) + IOrganizationRepository organizationRepository, + ISchedulerFactory schedulerFactory, + IFeatureService featureService) { _stripeEventService = stripeEventService; _stripeEventUtilityService = stripeEventUtilityService; @@ -38,6 +45,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler _userService = userService; _pushNotificationService = pushNotificationService; _organizationRepository = organizationRepository; + _schedulerFactory = schedulerFactory; + _featureService = featureService; } /// @@ -55,6 +64,10 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler when organizationId.HasValue: { await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); + if (subscription.Status == StripeSubscriptionStatus.Unpaid) + { + await ScheduleCancellationJobAsync(subscription.Id, organizationId.Value); + } break; } case StripeSubscriptionStatus.Unpaid or StripeSubscriptionStatus.IncompleteExpired: @@ -183,4 +196,27 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler await _stripeFacade.DeleteSubscriptionDiscount(subscription.Id); } } + + private async Task ScheduleCancellationJobAsync(string subscriptionId, Guid organizationId) + { + var isResellerManagedOrgAlertEnabled = _featureService.IsEnabled(FeatureFlagKeys.ResellerManagedOrgAlert); + + if (isResellerManagedOrgAlertEnabled) + { + var scheduler = await _schedulerFactory.GetScheduler(); + + var job = JobBuilder.Create() + .WithIdentity($"cancel-sub-{subscriptionId}", "subscription-cancellations") + .UsingJobData("subscriptionId", subscriptionId) + .UsingJobData("organizationId", organizationId.ToString()) + .Build(); + + var trigger = TriggerBuilder.Create() + .WithIdentity($"cancel-trigger-{subscriptionId}", "subscription-cancellations") + .StartAt(DateTimeOffset.UtcNow.AddDays(7)) + .Build(); + + await scheduler.ScheduleJob(job, trigger); + } + } } diff --git a/src/Billing/Startup.cs b/src/Billing/Startup.cs index e3547d943b..2d2f109e77 100644 --- a/src/Billing/Startup.cs +++ b/src/Billing/Startup.cs @@ -9,6 +9,7 @@ using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.SharedWeb.Utilities; using Microsoft.Extensions.DependencyInjection.Extensions; +using Quartz; using Stripe; namespace Bit.Billing; @@ -101,6 +102,13 @@ public class Startup services.AddScoped(); services.AddScoped(); + // Add Quartz services first + services.AddQuartz(q => + { + q.UseMicrosoftDependencyInjectionJobFactory(); + }); + services.AddQuartzHostedService(); + // Jobs service Jobs.JobsHostedService.AddJobsServices(services); services.AddHostedService(); diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index ddd9fc26bb..7a5f7e2543 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -67,6 +67,9 @@ + + + From 36c8a97d5606255df7ac82f29a8822b108548b1e Mon Sep 17 00:00:00 2001 From: Github Actions Date: Fri, 24 Jan 2025 15:20:02 +0000 Subject: [PATCH 052/148] Bumped version to 2025.1.4 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 40e6fdd202..9c54e35e6e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.1.3 + 2025.1.4 Bit.$(MSBuildProjectName) enable From 99a1dbbe02609c316d80c0003aa486a3c721b57c Mon Sep 17 00:00:00 2001 From: Graham Walker Date: Fri, 24 Jan 2025 10:57:44 -0600 Subject: [PATCH 053/148] PM-16261 move ImportCiphersAsync to the tools team (#5245) * PM-16261 move ImportCiphersAsync to the tools team and create services using CQRS design pattern * PM-16261 fix renaming methods and add unit tests for succes and bad request exception * PM-16261 clean up old code from test --- src/Api/Startup.cs | 2 + .../Controllers/ImportCiphersController.cs | 13 +- .../ImportFeatures/ImportCiphersCommand.cs | 199 ++++++++++++++++++ .../ImportServiceCollectionExtension.cs | 12 ++ .../Interfaces/IImportCiphersCommand.cs | 14 ++ src/Core/Vault/Services/ICipherService.cs | 4 - .../Services/Implementations/CipherService.cs | 146 ------------- .../Utilities/ServiceCollectionExtensions.cs | 2 + .../ImportCiphersAsyncCommandTests.cs | 181 ++++++++++++++++ .../Vault/Services/CipherServiceTests.cs | 61 ------ 10 files changed, 416 insertions(+), 218 deletions(-) create mode 100644 src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs create mode 100644 src/Core/Tools/ImportFeatures/ImportServiceCollectionExtension.cs create mode 100644 src/Core/Tools/ImportFeatures/Interfaces/IImportCiphersCommand.cs create mode 100644 test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 1adf3f67dc..a341257259 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -29,6 +29,7 @@ using Bit.Core.Vault.Entities; using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Identity.TokenProviders; +using Bit.Core.Tools.ImportFeatures; using Bit.Core.Tools.ReportFeatures; @@ -175,6 +176,7 @@ public class Startup services.AddCoreLocalizationServices(); services.AddBillingOperations(); services.AddReportingServices(); + services.AddImportServices(); // Authorization Handlers services.AddAuthorizationHandlers(); diff --git a/src/Api/Tools/Controllers/ImportCiphersController.cs b/src/Api/Tools/Controllers/ImportCiphersController.cs index 0d07d5bc47..4f4e76f6e3 100644 --- a/src/Api/Tools/Controllers/ImportCiphersController.cs +++ b/src/Api/Tools/Controllers/ImportCiphersController.cs @@ -7,7 +7,7 @@ using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Vault.Services; +using Bit.Core.Tools.ImportFeatures.Interfaces; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -17,31 +17,30 @@ namespace Bit.Api.Tools.Controllers; [Authorize("Application")] public class ImportCiphersController : Controller { - private readonly ICipherService _cipherService; private readonly IUserService _userService; private readonly ICurrentContext _currentContext; private readonly ILogger _logger; private readonly GlobalSettings _globalSettings; private readonly ICollectionRepository _collectionRepository; private readonly IAuthorizationService _authorizationService; + private readonly IImportCiphersCommand _importCiphersCommand; public ImportCiphersController( - ICipherService cipherService, IUserService userService, ICurrentContext currentContext, ILogger logger, GlobalSettings globalSettings, ICollectionRepository collectionRepository, IAuthorizationService authorizationService, - IOrganizationRepository organizationRepository) + IImportCiphersCommand importCiphersCommand) { - _cipherService = cipherService; _userService = userService; _currentContext = currentContext; _logger = logger; _globalSettings = globalSettings; _collectionRepository = collectionRepository; _authorizationService = authorizationService; + _importCiphersCommand = importCiphersCommand; } [HttpPost("import")] @@ -57,7 +56,7 @@ public class ImportCiphersController : Controller var userId = _userService.GetProperUserId(User).Value; var folders = model.Folders.Select(f => f.ToFolder(userId)).ToList(); var ciphers = model.Ciphers.Select(c => c.ToCipherDetails(userId, false)).ToList(); - await _cipherService.ImportCiphersAsync(folders, ciphers, model.FolderRelationships); + await _importCiphersCommand.ImportIntoIndividualVaultAsync(folders, ciphers, model.FolderRelationships); } [HttpPost("import-organization")] @@ -85,7 +84,7 @@ public class ImportCiphersController : Controller var userId = _userService.GetProperUserId(User).Value; var ciphers = model.Ciphers.Select(l => l.ToOrganizationCipherDetails(orgId)).ToList(); - await _cipherService.ImportCiphersAsync(collections, ciphers, model.CollectionRelationships, userId); + await _importCiphersCommand.ImportIntoOrganizationalVaultAsync(collections, ciphers, model.CollectionRelationships, userId); } private async Task CheckOrgImportPermission(List collections, Guid orgId) diff --git a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs new file mode 100644 index 0000000000..646121db52 --- /dev/null +++ b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs @@ -0,0 +1,199 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Services; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; +using Bit.Core.Repositories; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.ImportFeatures.Interfaces; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Tools.Services; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Repositories; + +namespace Bit.Core.Tools.ImportFeatures; + +public class ImportCiphersCommand : IImportCiphersCommand +{ + private readonly ICipherRepository _cipherRepository; + private readonly IFolderRepository _folderRepository; + private readonly IPushNotificationService _pushService; + private readonly IPolicyService _policyService; + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly ICollectionRepository _collectionRepository; + private readonly IReferenceEventService _referenceEventService; + private readonly ICurrentContext _currentContext; + + + public ImportCiphersCommand( + ICipherRepository cipherRepository, + IFolderRepository folderRepository, + ICollectionRepository collectionRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IPushNotificationService pushService, + IPolicyService policyService, + IReferenceEventService referenceEventService, + ICurrentContext currentContext) + { + _cipherRepository = cipherRepository; + _folderRepository = folderRepository; + _organizationRepository = organizationRepository; + _organizationUserRepository = organizationUserRepository; + _collectionRepository = collectionRepository; + _pushService = pushService; + _policyService = policyService; + _referenceEventService = referenceEventService; + _currentContext = currentContext; + } + + + public async Task ImportIntoIndividualVaultAsync( + List folders, + List ciphers, + IEnumerable> folderRelationships) + { + var userId = folders.FirstOrDefault()?.UserId ?? ciphers.FirstOrDefault()?.UserId; + + // Make sure the user can save new ciphers to their personal vault + var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value, PolicyType.PersonalOwnership); + if (anyPersonalOwnershipPolicies) + { + throw new BadRequestException("You cannot import items into your personal vault because you are " + + "a member of an organization which forbids it."); + } + + foreach (var cipher in ciphers) + { + cipher.SetNewId(); + + if (cipher.UserId.HasValue && cipher.Favorite) + { + cipher.Favorites = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":\"true\"}}"; + } + } + + var userfoldersIds = (await _folderRepository.GetManyByUserIdAsync(userId ?? Guid.Empty)).Select(f => f.Id).ToList(); + + //Assign id to the ones that don't exist in DB + //Need to keep the list order to create the relationships + List newFolders = new List(); + foreach (var folder in folders) + { + if (!userfoldersIds.Contains(folder.Id)) + { + folder.SetNewId(); + newFolders.Add(folder); + } + } + + // Create the folder associations based on the newly created folder ids + foreach (var relationship in folderRelationships) + { + var cipher = ciphers.ElementAtOrDefault(relationship.Key); + var folder = folders.ElementAtOrDefault(relationship.Value); + + if (cipher == null || folder == null) + { + continue; + } + + cipher.Folders = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":" + + $"\"{folder.Id.ToString().ToUpperInvariant()}\"}}"; + } + + // Create it all + await _cipherRepository.CreateAsync(ciphers, newFolders); + + // push + if (userId.HasValue) + { + await _pushService.PushSyncVaultAsync(userId.Value); + } + } + + public async Task ImportIntoOrganizationalVaultAsync( + List collections, + List ciphers, + IEnumerable> collectionRelationships, + Guid importingUserId) + { + var org = collections.Count > 0 ? + await _organizationRepository.GetByIdAsync(collections[0].OrganizationId) : + await _organizationRepository.GetByIdAsync(ciphers.FirstOrDefault(c => c.OrganizationId.HasValue).OrganizationId.Value); + var importingOrgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, importingUserId); + + if (collections.Count > 0 && org != null && org.MaxCollections.HasValue) + { + var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(org.Id); + if (org.MaxCollections.Value < (collectionCount + collections.Count)) + { + throw new BadRequestException("This organization can only have a maximum of " + + $"{org.MaxCollections.Value} collections."); + } + } + + // Init. ids for ciphers + foreach (var cipher in ciphers) + { + cipher.SetNewId(); + } + + var organizationCollectionsIds = (await _collectionRepository.GetManyByOrganizationIdAsync(org.Id)).Select(c => c.Id).ToList(); + + //Assign id to the ones that don't exist in DB + //Need to keep the list order to create the relationships + var newCollections = new List(); + var newCollectionUsers = new List(); + + foreach (var collection in collections) + { + if (!organizationCollectionsIds.Contains(collection.Id)) + { + collection.SetNewId(); + newCollections.Add(collection); + newCollectionUsers.Add(new CollectionUser + { + CollectionId = collection.Id, + OrganizationUserId = importingOrgUser.Id, + Manage = true + }); + } + } + + // Create associations based on the newly assigned ids + var collectionCiphers = new List(); + foreach (var relationship in collectionRelationships) + { + var cipher = ciphers.ElementAtOrDefault(relationship.Key); + var collection = collections.ElementAtOrDefault(relationship.Value); + + if (cipher == null || collection == null) + { + continue; + } + + collectionCiphers.Add(new CollectionCipher + { + CipherId = cipher.Id, + CollectionId = collection.Id + }); + } + + // Create it all + await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers); + + // push + await _pushService.PushSyncVaultAsync(importingUserId); + + + if (org != null) + { + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.VaultImported, org, _currentContext)); + } + } +} diff --git a/src/Core/Tools/ImportFeatures/ImportServiceCollectionExtension.cs b/src/Core/Tools/ImportFeatures/ImportServiceCollectionExtension.cs new file mode 100644 index 0000000000..38c88d7994 --- /dev/null +++ b/src/Core/Tools/ImportFeatures/ImportServiceCollectionExtension.cs @@ -0,0 +1,12 @@ +using Bit.Core.Tools.ImportFeatures.Interfaces; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Tools.ImportFeatures; + +public static class ImportServiceCollectionExtension +{ + public static void AddImportServices(this IServiceCollection services) + { + services.AddScoped(); + } +} diff --git a/src/Core/Tools/ImportFeatures/Interfaces/IImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/Interfaces/IImportCiphersCommand.cs new file mode 100644 index 0000000000..378024d3a0 --- /dev/null +++ b/src/Core/Tools/ImportFeatures/Interfaces/IImportCiphersCommand.cs @@ -0,0 +1,14 @@ +using Bit.Core.Entities; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Models.Data; + +namespace Bit.Core.Tools.ImportFeatures.Interfaces; + +public interface IImportCiphersCommand +{ + Task ImportIntoIndividualVaultAsync(List folders, List ciphers, + IEnumerable> folderRelationships); + + Task ImportIntoOrganizationalVaultAsync(List collections, List ciphers, + IEnumerable> collectionRelationships, Guid importingUserId); +} diff --git a/src/Core/Vault/Services/ICipherService.cs b/src/Core/Vault/Services/ICipherService.cs index 27b84e4a47..e559963361 100644 --- a/src/Core/Vault/Services/ICipherService.cs +++ b/src/Core/Vault/Services/ICipherService.cs @@ -28,10 +28,6 @@ public interface ICipherService Task ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> ciphers, Guid organizationId, IEnumerable collectionIds, Guid sharingUserId); Task SaveCollectionsAsync(Cipher cipher, IEnumerable collectionIds, Guid savingUserId, bool orgAdmin); - Task ImportCiphersAsync(List folders, List ciphers, - IEnumerable> folderRelationships); - Task ImportCiphersAsync(List collections, List ciphers, - IEnumerable> collectionRelationships, Guid importingUserId); Task SoftDeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false); Task SoftDeleteManyAsync(IEnumerable cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false); Task RestoreAsync(Cipher cipher, Guid restoringUserId, bool orgAdmin = false); diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index 196ec6ef3d..da1a5f978f 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -679,152 +679,6 @@ public class CipherService : ICipherService await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds); } - public async Task ImportCiphersAsync( - List folders, - List ciphers, - IEnumerable> folderRelationships) - { - var userId = folders.FirstOrDefault()?.UserId ?? ciphers.FirstOrDefault()?.UserId; - - // Make sure the user can save new ciphers to their personal vault - var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value, PolicyType.PersonalOwnership); - if (anyPersonalOwnershipPolicies) - { - throw new BadRequestException("You cannot import items into your personal vault because you are " + - "a member of an organization which forbids it."); - } - - foreach (var cipher in ciphers) - { - cipher.SetNewId(); - - if (cipher.UserId.HasValue && cipher.Favorite) - { - cipher.Favorites = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":\"true\"}}"; - } - } - - var userfoldersIds = (await _folderRepository.GetManyByUserIdAsync(userId ?? Guid.Empty)).Select(f => f.Id).ToList(); - - //Assign id to the ones that don't exist in DB - //Need to keep the list order to create the relationships - List newFolders = new List(); - foreach (var folder in folders) - { - if (!userfoldersIds.Contains(folder.Id)) - { - folder.SetNewId(); - newFolders.Add(folder); - } - } - - // Create the folder associations based on the newly created folder ids - foreach (var relationship in folderRelationships) - { - var cipher = ciphers.ElementAtOrDefault(relationship.Key); - var folder = folders.ElementAtOrDefault(relationship.Value); - - if (cipher == null || folder == null) - { - continue; - } - - cipher.Folders = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":" + - $"\"{folder.Id.ToString().ToUpperInvariant()}\"}}"; - } - - // Create it all - await _cipherRepository.CreateAsync(ciphers, newFolders); - - // push - if (userId.HasValue) - { - await _pushService.PushSyncVaultAsync(userId.Value); - } - } - - public async Task ImportCiphersAsync( - List collections, - List ciphers, - IEnumerable> collectionRelationships, - Guid importingUserId) - { - var org = collections.Count > 0 ? - await _organizationRepository.GetByIdAsync(collections[0].OrganizationId) : - await _organizationRepository.GetByIdAsync(ciphers.FirstOrDefault(c => c.OrganizationId.HasValue).OrganizationId.Value); - var importingOrgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, importingUserId); - - if (collections.Count > 0 && org != null && org.MaxCollections.HasValue) - { - var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(org.Id); - if (org.MaxCollections.Value < (collectionCount + collections.Count)) - { - throw new BadRequestException("This organization can only have a maximum of " + - $"{org.MaxCollections.Value} collections."); - } - } - - // Init. ids for ciphers - foreach (var cipher in ciphers) - { - cipher.SetNewId(); - } - - var organizationCollectionsIds = (await _collectionRepository.GetManyByOrganizationIdAsync(org.Id)).Select(c => c.Id).ToList(); - - //Assign id to the ones that don't exist in DB - //Need to keep the list order to create the relationships - var newCollections = new List(); - var newCollectionUsers = new List(); - - foreach (var collection in collections) - { - if (!organizationCollectionsIds.Contains(collection.Id)) - { - collection.SetNewId(); - newCollections.Add(collection); - newCollectionUsers.Add(new CollectionUser - { - CollectionId = collection.Id, - OrganizationUserId = importingOrgUser.Id, - Manage = true - }); - } - } - - // Create associations based on the newly assigned ids - var collectionCiphers = new List(); - foreach (var relationship in collectionRelationships) - { - var cipher = ciphers.ElementAtOrDefault(relationship.Key); - var collection = collections.ElementAtOrDefault(relationship.Value); - - if (cipher == null || collection == null) - { - continue; - } - - collectionCiphers.Add(new CollectionCipher - { - CipherId = cipher.Id, - CollectionId = collection.Id - }); - } - - // Create it all - await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers); - - // push - await _pushService.PushSyncVaultAsync(importingUserId); - - - if (org != null) - { - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.VaultImported, org, _currentContext)); - } - } - public async Task SoftDeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false) { if (!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId))) diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 63c114405f..e1369d5366 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -40,6 +40,7 @@ using Bit.Core.SecretsManager.Repositories.Noop; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tokens; +using Bit.Core.Tools.ImportFeatures; using Bit.Core.Tools.ReportFeatures; using Bit.Core.Tools.Services; using Bit.Core.Utilities; @@ -128,6 +129,7 @@ public static class ServiceCollectionExtensions services.AddKeyManagementServices(); services.AddNotificationCenterServices(); services.AddPlatformServices(); + services.AddImportServices(); } public static void AddTokenizers(this IServiceCollection services) diff --git a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs new file mode 100644 index 0000000000..1e97856281 --- /dev/null +++ b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs @@ -0,0 +1,181 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Services; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; +using Bit.Core.Repositories; +using Bit.Core.Test.AutoFixture.CipherFixtures; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.ImportFeatures; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Tools.Services; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + + +namespace Bit.Core.Test.Tools.ImportFeatures; + +[UserCipherCustomize] +[SutProviderCustomize] +public class ImportCiphersAsyncCommandTests +{ + [Theory, BitAutoData] + public async Task ImportIntoIndividualVaultAsync_Success( + Guid importingUserId, + List ciphers, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.PersonalOwnership) + .Returns(false); + + sutProvider.GetDependency() + .GetManyByUserIdAsync(importingUserId) + .Returns(new List()); + + var folders = new List { new Folder { UserId = importingUserId } }; + + var folderRelationships = new List>(); + + // Act + await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships); + + // Assert + await sutProvider.GetDependency().Received(1).CreateAsync(ciphers, Arg.Any>()); + await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); + } + + [Theory, BitAutoData] + public async Task ImportIntoIndividualVaultAsync_ThrowsBadRequestException( + List folders, + List ciphers, + SutProvider sutProvider) + { + var userId = Guid.NewGuid(); + folders.ForEach(f => f.UserId = userId); + ciphers.ForEach(c => c.UserId = userId); + + sutProvider.GetDependency() + .AnyPoliciesApplicableToUserAsync(userId, PolicyType.PersonalOwnership) + .Returns(true); + + var folderRelationships = new List>(); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships)); + + Assert.Equal("You cannot import items into your personal vault because you are a member of an organization which forbids it.", exception.Message); + } + + [Theory, BitAutoData] + public async Task ImportIntoOrganizationalVaultAsync_Success( + Organization organization, + Guid importingUserId, + OrganizationUser importingOrganizationUser, + List collections, + List ciphers, + SutProvider sutProvider) + { + organization.MaxCollections = null; + importingOrganizationUser.OrganizationId = organization.Id; + + foreach (var collection in collections) + { + collection.OrganizationId = organization.Id; + } + + foreach (var cipher in ciphers) + { + cipher.OrganizationId = organization.Id; + } + + KeyValuePair[] collectionRelationships = { + new(0, 0), + new(1, 1), + new(2, 2) + }; + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + sutProvider.GetDependency() + .GetByOrganizationAsync(organization.Id, importingUserId) + .Returns(importingOrganizationUser); + + // Set up a collection that already exists in the organization + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(organization.Id) + .Returns(new List { collections[0] }); + + await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId); + + await sutProvider.GetDependency().Received(1).CreateAsync( + ciphers, + Arg.Is>(cols => cols.Count() == collections.Count - 1 && + !cols.Any(c => c.Id == collections[0].Id) && // Check that the collection that already existed in the organization was not added + cols.All(c => collections.Any(x => c.Name == x.Name))), + Arg.Is>(c => c.Count() == ciphers.Count), + Arg.Is>(cus => + cus.Count() == collections.Count - 1 && + !cus.Any(cu => cu.CollectionId == collections[0].Id) && // Check that access was not added for the collection that already existed in the organization + cus.All(cu => cu.OrganizationUserId == importingOrganizationUser.Id && cu.Manage == true))); + await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); + await sutProvider.GetDependency().Received(1).RaiseEventAsync( + Arg.Is(e => e.Type == ReferenceEventType.VaultImported)); + } + + [Theory, BitAutoData] + public async Task ImportIntoOrganizationalVaultAsync_ThrowsBadRequestException( + Organization organization, + Guid importingUserId, + OrganizationUser importingOrganizationUser, + List collections, + List ciphers, + SutProvider sutProvider) + { + organization.MaxCollections = 1; + importingOrganizationUser.OrganizationId = organization.Id; + + foreach (var collection in collections) + { + collection.OrganizationId = organization.Id; + } + + foreach (var cipher in ciphers) + { + cipher.OrganizationId = organization.Id; + } + + KeyValuePair[] collectionRelationships = { + new(0, 0), + new(1, 1), + new(2, 2) + }; + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + sutProvider.GetDependency() + .GetByOrganizationAsync(organization.Id, importingUserId) + .Returns(importingOrganizationUser); + + // Set up a collection that already exists in the organization + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(organization.Id) + .Returns(new List { collections[0] }); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId)); + + Assert.Equal("This organization can only have a maximum of " + + $"{organization.MaxCollections} collections.", exception.Message); + } +} diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs index dd34127efe..4f02d94c9c 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -7,9 +7,6 @@ using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Test.AutoFixture.CipherFixtures; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; @@ -26,64 +23,6 @@ namespace Bit.Core.Test.Services; [SutProviderCustomize] public class CipherServiceTests { - [Theory, BitAutoData] - public async Task ImportCiphersAsync_IntoOrganization_Success( - Organization organization, - Guid importingUserId, - OrganizationUser importingOrganizationUser, - List collections, - List ciphers, - SutProvider sutProvider) - { - organization.MaxCollections = null; - importingOrganizationUser.OrganizationId = organization.Id; - - foreach (var collection in collections) - { - collection.OrganizationId = organization.Id; - } - - foreach (var cipher in ciphers) - { - cipher.OrganizationId = organization.Id; - } - - KeyValuePair[] collectionRelationships = { - new(0, 0), - new(1, 1), - new(2, 2) - }; - - sutProvider.GetDependency() - .GetByIdAsync(organization.Id) - .Returns(organization); - - sutProvider.GetDependency() - .GetByOrganizationAsync(organization.Id, importingUserId) - .Returns(importingOrganizationUser); - - // Set up a collection that already exists in the organization - sutProvider.GetDependency() - .GetManyByOrganizationIdAsync(organization.Id) - .Returns(new List { collections[0] }); - - await sutProvider.Sut.ImportCiphersAsync(collections, ciphers, collectionRelationships, importingUserId); - - await sutProvider.GetDependency().Received(1).CreateAsync( - ciphers, - Arg.Is>(cols => cols.Count() == collections.Count - 1 && - !cols.Any(c => c.Id == collections[0].Id) && // Check that the collection that already existed in the organization was not added - cols.All(c => collections.Any(x => c.Name == x.Name))), - Arg.Is>(c => c.Count() == ciphers.Count), - Arg.Is>(cus => - cus.Count() == collections.Count - 1 && - !cus.Any(cu => cu.CollectionId == collections[0].Id) && // Check that access was not added for the collection that already existed in the organization - cus.All(cu => cu.OrganizationUserId == importingOrganizationUser.Id && cu.Manage == true))); - await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); - await sutProvider.GetDependency().Received(1).RaiseEventAsync( - Arg.Is(e => e.Type == ReferenceEventType.VaultImported)); - } - [Theory, BitAutoData] public async Task SaveAsync_WrongRevisionDate_Throws(SutProvider sutProvider, Cipher cipher) { From f140c7f6c1cb365916ac1addac478295180e9903 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Fri, 24 Jan 2025 13:38:35 -0500 Subject: [PATCH 054/148] [PM-11730] Remove feature flag: AC-2476-deprecate-stripe-sources-api (#5201) * Removed feature flag * Run dotnet format * Fix integration tests --- .../OrganizationBillingController.cs | 31 ----------- .../CloudOrganizationSignUpCommand.cs | 24 +-------- src/Core/Constants.cs | 1 - .../UpgradeOrganizationPlanCommand.cs | 23 +------- .../Services/Implementations/UserService.cs | 14 +---- .../Helpers/OrganizationTestHelpers.cs | 8 ++- .../CloudOrganizationSignUpCommandTests.cs | 53 ++++++++----------- .../UpgradeOrganizationPlanCommandTests.cs | 2 +- .../Factories/WebApplicationFactoryBase.cs | 6 +++ 9 files changed, 43 insertions(+), 119 deletions(-) diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 4a6f5f5b8a..b52241c30e 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -2,7 +2,6 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; -using Bit.Core; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Services; using Bit.Core.Context; @@ -139,11 +138,6 @@ public class OrganizationBillingController( [HttpGet("payment-method")] public async Task GetPaymentMethodAsync([FromRoute] Guid organizationId) { - if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI)) - { - return Error.NotFound(); - } - if (!await currentContext.EditPaymentMethods(organizationId)) { return Error.Unauthorized(); @@ -168,11 +162,6 @@ public class OrganizationBillingController( [FromRoute] Guid organizationId, [FromBody] UpdatePaymentMethodRequestBody requestBody) { - if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI)) - { - return Error.NotFound(); - } - if (!await currentContext.EditPaymentMethods(organizationId)) { return Error.Unauthorized(); @@ -199,11 +188,6 @@ public class OrganizationBillingController( [FromRoute] Guid organizationId, [FromBody] VerifyBankAccountRequestBody requestBody) { - if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI)) - { - return Error.NotFound(); - } - if (!await currentContext.EditPaymentMethods(organizationId)) { return Error.Unauthorized(); @@ -229,11 +213,6 @@ public class OrganizationBillingController( [HttpGet("tax-information")] public async Task GetTaxInformationAsync([FromRoute] Guid organizationId) { - if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI)) - { - return Error.NotFound(); - } - if (!await currentContext.EditPaymentMethods(organizationId)) { return Error.Unauthorized(); @@ -258,11 +237,6 @@ public class OrganizationBillingController( [FromRoute] Guid organizationId, [FromBody] TaxInformationRequestBody requestBody) { - if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI)) - { - return Error.NotFound(); - } - if (!await currentContext.EditPaymentMethods(organizationId)) { return Error.Unauthorized(); @@ -292,11 +266,6 @@ public class OrganizationBillingController( throw new UnauthorizedAccessException(); } - if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI)) - { - return Error.NotFound(); - } - if (!await currentContext.EditPaymentMethods(organizationId)) { return Error.Unauthorized(); diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs index df841adf42..94ff3c0059 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs @@ -125,28 +125,8 @@ public class CloudOrganizationSignUpCommand( } 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 sale = OrganizationSale.From(organization, signup); + await organizationBillingService.Finalize(sale); } var ownerId = signup.IsFromProvider ? default : signup.Owner.Id; diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index dd45593ae9..dea65b929e 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -137,7 +137,6 @@ public static class FeatureFlagKeys public const string NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements"; public const string BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain"; public const string NotificationRefresh = "notification-refresh"; - public const string AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api"; public const string PersistPopupView = "persist-popup-view"; public const string CipherKeyEncryption = "cipher-key-encryption"; public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill"; diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs index 7f463460dd..19af8121e7 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs @@ -224,27 +224,8 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) { - if (_featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI)) - { - var sale = OrganizationSale.From(organization, upgrade); - await _organizationBillingService.Finalize(sale); - } - else - { - try - { - paymentIntentClientSecret = await _paymentService.UpgradeFreeOrganizationAsync(organization, - newPlan, upgrade); - success = string.IsNullOrWhiteSpace(paymentIntentClientSecret); - } - catch - { - await _paymentService.CancelAndRecoverChargesAsync(organization); - organization.GatewayCustomerId = null; - await _organizationService.ReplaceAndUpdateCacheAsync(organization); - throw; - } - } + var sale = OrganizationSale.From(organization, upgrade); + await _organizationBillingService.Finalize(sale); } else { diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 1dd8c3f8ca..157bfd3a6e 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -933,18 +933,8 @@ public class UserService : UserManager, IUserService, IDisposable } else { - var deprecateStripeSourcesAPI = _featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI); - - if (deprecateStripeSourcesAPI) - { - var sale = PremiumUserSale.From(user, paymentMethodType, paymentToken, taxInfo, additionalStorageGb); - await _premiumUserBillingService.Finalize(sale); - } - else - { - paymentIntentClientSecret = await _paymentService.PurchasePremiumAsync(user, paymentMethodType, - paymentToken, additionalStorageGb, taxInfo); - } + var sale = PremiumUserSale.From(user, paymentMethodType, paymentToken, taxInfo, additionalStorageGb); + await _premiumUserBillingService.Finalize(sale); } user.Premium = true; diff --git a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs index dd514803fe..9370948a85 100644 --- a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs +++ b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs @@ -36,7 +36,13 @@ public static class OrganizationTestHelpers OwnerKey = ownerKey, Owner = owner, AdditionalSeats = passwordManagerSeats, - PaymentMethodType = paymentMethod + PaymentMethodType = paymentMethod, + PaymentToken = "TOKEN", + TaxInfo = new TaxInfo + { + BillingAddressCountry = "US", + BillingAddressPostalCode = "12345" + } }); Debug.Assert(signUpResult.OrganizationUser is not null); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs index 46b4f0b334..859c74f3d0 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs @@ -1,14 +1,14 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Services; 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; @@ -60,20 +60,16 @@ public class CloudICloudOrganizationSignUpCommandTests 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 - ); + await sutProvider.GetDependency().Received(1).Finalize( + Arg.Is(sale => + sale.CustomerSetup.TokenizedPaymentSource.Type == signup.PaymentMethodType.Value && + sale.CustomerSetup.TokenizedPaymentSource.Token == signup.PaymentToken && + sale.CustomerSetup.TaxInformation.Country == signup.TaxInfo.BillingAddressCountry && + sale.CustomerSetup.TaxInformation.PostalCode == signup.TaxInfo.BillingAddressPostalCode && + sale.SubscriptionSetup.Plan == plan && + sale.SubscriptionSetup.PasswordManagerOptions.Seats == signup.AdditionalSeats && + sale.SubscriptionSetup.PasswordManagerOptions.Storage == signup.AdditionalStorageGb && + sale.SubscriptionSetup.SecretsManagerOptions == null)); } [Theory] @@ -155,20 +151,17 @@ public class CloudICloudOrganizationSignUpCommandTests 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 - ); + await sutProvider.GetDependency().Received(1).Finalize( + Arg.Is(sale => + sale.CustomerSetup.TokenizedPaymentSource.Type == signup.PaymentMethodType.Value && + sale.CustomerSetup.TokenizedPaymentSource.Token == signup.PaymentToken && + sale.CustomerSetup.TaxInformation.Country == signup.TaxInfo.BillingAddressCountry && + sale.CustomerSetup.TaxInformation.PostalCode == signup.TaxInfo.BillingAddressPostalCode && + sale.SubscriptionSetup.Plan == plan && + sale.SubscriptionSetup.PasswordManagerOptions.Seats == signup.AdditionalSeats && + sale.SubscriptionSetup.PasswordManagerOptions.Storage == signup.AdditionalStorageGb && + sale.SubscriptionSetup.SecretsManagerOptions.Seats == signup.AdditionalSmSeats && + sale.SubscriptionSetup.SecretsManagerOptions.ServiceAccounts == signup.AdditionalServiceAccounts)); } [Theory] diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs index 0f47b6c921..2965a2f03d 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs @@ -139,7 +139,7 @@ public class UpgradeOrganizationPlanCommandTests && o.SmServiceAccounts == plan.SecretsManager.BaseServiceAccount + upgrade.AdditionalServiceAccounts)); Assert.True(result.Item1); - Assert.NotNull(result.Item2); + Assert.Null(result.Item2); } [Theory, FreeOrganizationUpgradeCustomize] diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs index 9474ffb862..d01e92ad4c 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs @@ -1,5 +1,6 @@ using AspNetCoreRateLimit; using Bit.Core.Auth.Services; +using Bit.Core.Billing.Services; using Bit.Core.Platform.Push; using Bit.Core.Platform.Push.Internal; using Bit.Core.Repositories; @@ -247,6 +248,11 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory var stripePaymentService = services.First(sd => sd.ServiceType == typeof(IPaymentService)); services.Remove(stripePaymentService); services.AddSingleton(Substitute.For()); + + var organizationBillingService = + services.First(sd => sd.ServiceType == typeof(IOrganizationBillingService)); + services.Remove(organizationBillingService); + services.AddSingleton(Substitute.For()); }); foreach (var configureTestService in _configureTestServices) From f2182c2aae876fc44f0e8dad9597b858c51800b4 Mon Sep 17 00:00:00 2001 From: Graham Walker Date: Fri, 24 Jan 2025 13:43:41 -0600 Subject: [PATCH 055/148] PM-16261 fixing linter issue (#5322) --- src/Core/Vault/Services/ICipherService.cs | 3 +-- src/Core/Vault/Services/Implementations/CipherService.cs | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Core/Vault/Services/ICipherService.cs b/src/Core/Vault/Services/ICipherService.cs index e559963361..b332fbb96f 100644 --- a/src/Core/Vault/Services/ICipherService.cs +++ b/src/Core/Vault/Services/ICipherService.cs @@ -1,5 +1,4 @@ -using Bit.Core.Entities; -using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; namespace Bit.Core.Vault.Services; diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index da1a5f978f..ea4db01fd7 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -2,7 +2,6 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; using Bit.Core.Context; -using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Platform.Push; From 3908edd08fcff620b3c9709d6e4be1633267a096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:58:08 +0000 Subject: [PATCH 056/148] [PM-12489] Extract OrganizationService.DeleteAsync and OrganizationService.InitiateDeleteAsync into commands (#5279) * Create organization deletion command with logic extracted from OrganizationService * Add unit tests for OrganizationDeleteCommand * Register OrganizationDeleteCommand for dependency injection * Refactor organization deletion logic to use IOrganizationDeleteCommand and remove legacy IOrganizationService.DeleteAsync method * Add organization deletion initiation command and refactor service usage * Enhance organization deletion commands with detailed XML documentation * Refactor organization command registration to include sign-up and deletion methods --- .../Controllers/OrganizationsController.cs | 8 +- .../Controllers/OrganizationsController.cs | 10 +- .../Interfaces/IOrganizationDeleteCommand.cs | 13 ++ .../IOrganizationInitiateDeleteCommand.cs | 14 +++ .../OrganizationDeleteCommand.cs | 69 +++++++++++ .../OrganizationInitiateDeleteCommand.cs | 49 ++++++++ .../Services/IOrganizationService.cs | 2 - .../Implementations/OrganizationService.cs | 51 -------- ...OrganizationServiceCollectionExtensions.cs | 8 ++ .../OrganizationsControllerTests.cs | 8 +- .../OrganizationDeleteCommandTests.cs | 53 +++++++++ .../OrganizationInitiateDeleteCommandTests.cs | 112 ++++++++++++++++++ .../Services/OrganizationServiceTests.cs | 35 ------ 13 files changed, 337 insertions(+), 95 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationDeleteCommand.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationInitiateDeleteCommand.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommand.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationInitiateDeleteCommand.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommandTests.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationInitiateDeleteCommandTests.cs diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index 86aebfaad7..b24226ee35 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -5,6 +5,7 @@ using Bit.Admin.Services; using Bit.Admin.Utilities; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; @@ -56,6 +57,7 @@ public class OrganizationsController : Controller private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand; private readonly IProviderBillingService _providerBillingService; private readonly IFeatureService _featureService; + private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand; public OrganizationsController( IOrganizationService organizationService, @@ -82,7 +84,8 @@ public class OrganizationsController : Controller IProviderOrganizationRepository providerOrganizationRepository, IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand, IProviderBillingService providerBillingService, - IFeatureService featureService) + IFeatureService featureService, + IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand) { _organizationService = organizationService; _organizationRepository = organizationRepository; @@ -109,6 +112,7 @@ public class OrganizationsController : Controller _removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand; _providerBillingService = providerBillingService; _featureService = featureService; + _organizationInitiateDeleteCommand = organizationInitiateDeleteCommand; } [RequirePermission(Permission.Org_List_View)] @@ -319,7 +323,7 @@ public class OrganizationsController : Controller var organization = await _organizationRepository.GetByIdAsync(id); if (organization != null) { - await _organizationService.InitiateDeleteAsync(organization, model.AdminEmail); + await _organizationInitiateDeleteCommand.InitiateDeleteAsync(organization, model.AdminEmail); TempData["Success"] = "The request to initiate deletion of the organization has been sent."; } } diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 4e01bb3451..85e8e990a6 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -14,6 +14,7 @@ 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.Organizations.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; @@ -58,6 +59,7 @@ public class OrganizationsController : Controller private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand; + private readonly IOrganizationDeleteCommand _organizationDeleteCommand; public OrganizationsController( IOrganizationRepository organizationRepository, @@ -78,7 +80,8 @@ public class OrganizationsController : Controller IProviderBillingService providerBillingService, IDataProtectorTokenFactory orgDeleteTokenDataFactory, IRemoveOrganizationUserCommand removeOrganizationUserCommand, - ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand) + ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand, + IOrganizationDeleteCommand organizationDeleteCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -99,6 +102,7 @@ public class OrganizationsController : Controller _orgDeleteTokenDataFactory = orgDeleteTokenDataFactory; _removeOrganizationUserCommand = removeOrganizationUserCommand; _cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand; + _organizationDeleteCommand = organizationDeleteCommand; } [HttpGet("{id}")] @@ -303,7 +307,7 @@ public class OrganizationsController : Controller } } - await _organizationService.DeleteAsync(organization); + await _organizationDeleteCommand.DeleteAsync(organization); } [HttpPost("{id}/delete-recover-token")] @@ -333,7 +337,7 @@ public class OrganizationsController : Controller } } - await _organizationService.DeleteAsync(organization); + await _organizationDeleteCommand.DeleteAsync(organization); } [HttpPost("{id}/api-key")] diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationDeleteCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationDeleteCommand.cs new file mode 100644 index 0000000000..fc4de42bed --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationDeleteCommand.cs @@ -0,0 +1,13 @@ +using Bit.Core.AdminConsole.Entities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; + +public interface IOrganizationDeleteCommand +{ + /// + /// Permanently deletes an organization and performs necessary cleanup. + /// + /// The organization to delete. + /// Thrown when the organization cannot be deleted due to configuration constraints. + Task DeleteAsync(Organization organization); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationInitiateDeleteCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationInitiateDeleteCommand.cs new file mode 100644 index 0000000000..a8d211f245 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationInitiateDeleteCommand.cs @@ -0,0 +1,14 @@ +using Bit.Core.AdminConsole.Entities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; + +public interface IOrganizationInitiateDeleteCommand +{ + /// + /// Initiates a secure deletion process for an organization by requesting confirmation from an organization admin. + /// + /// The organization to be deleted. + /// The email address of the organization admin who will confirm the deletion. + /// Thrown when the specified admin email is invalid or lacks sufficient permissions. + Task InitiateDeleteAsync(Organization organization, string orgAdminEmail); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommand.cs new file mode 100644 index 0000000000..185d5c5ac0 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommand.cs @@ -0,0 +1,69 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Repositories; +using Bit.Core.Context; +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; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; + +public class OrganizationDeleteCommand : IOrganizationDeleteCommand +{ + private readonly IApplicationCacheService _applicationCacheService; + private readonly ICurrentContext _currentContext; + private readonly IOrganizationRepository _organizationRepository; + private readonly IPaymentService _paymentService; + private readonly IReferenceEventService _referenceEventService; + private readonly ISsoConfigRepository _ssoConfigRepository; + + public OrganizationDeleteCommand( + IApplicationCacheService applicationCacheService, + ICurrentContext currentContext, + IOrganizationRepository organizationRepository, + IPaymentService paymentService, + IReferenceEventService referenceEventService, + ISsoConfigRepository ssoConfigRepository) + { + _applicationCacheService = applicationCacheService; + _currentContext = currentContext; + _organizationRepository = organizationRepository; + _paymentService = paymentService; + _referenceEventService = referenceEventService; + _ssoConfigRepository = ssoConfigRepository; + } + + public async Task DeleteAsync(Organization organization) + { + await ValidateDeleteOrganizationAsync(organization); + + if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) + { + try + { + var eop = !organization.ExpirationDate.HasValue || + organization.ExpirationDate.Value >= DateTime.UtcNow; + await _paymentService.CancelSubscriptionAsync(organization, eop); + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.DeleteAccount, organization, _currentContext)); + } + catch (GatewayException) { } + } + + await _organizationRepository.DeleteAsync(organization); + await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id); + } + + private async Task ValidateDeleteOrganizationAsync(Organization organization) + { + var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id); + if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector) + { + throw new BadRequestException("You cannot delete an Organization that is using Key Connector."); + } + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationInitiateDeleteCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationInitiateDeleteCommand.cs new file mode 100644 index 0000000000..5979adc376 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationInitiateDeleteCommand.cs @@ -0,0 +1,49 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Business.Tokenables; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tokens; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; + +public class OrganizationInitiateDeleteCommand : IOrganizationInitiateDeleteCommand +{ + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IUserRepository _userRepository; + private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; + private readonly IMailService _mailService; + + public const string OrganizationAdminNotFoundErrorMessage = "Org admin not found."; + + public OrganizationInitiateDeleteCommand( + IOrganizationUserRepository organizationUserRepository, + IUserRepository userRepository, + IDataProtectorTokenFactory orgDeleteTokenDataFactory, + IMailService mailService) + { + _organizationUserRepository = organizationUserRepository; + _userRepository = userRepository; + _orgDeleteTokenDataFactory = orgDeleteTokenDataFactory; + _mailService = mailService; + } + + public async Task InitiateDeleteAsync(Organization organization, string orgAdminEmail) + { + var orgAdmin = await _userRepository.GetByEmailAsync(orgAdminEmail); + if (orgAdmin == null) + { + throw new BadRequestException(OrganizationAdminNotFoundErrorMessage); + } + var orgAdminOrgUser = await _organizationUserRepository.GetDetailsByUserAsync(orgAdmin.Id, organization.Id); + if (orgAdminOrgUser == null || orgAdminOrgUser.Status is not OrganizationUserStatusType.Confirmed || + (orgAdminOrgUser.Type is not OrganizationUserType.Admin and not OrganizationUserType.Owner)) + { + throw new BadRequestException(OrganizationAdminNotFoundErrorMessage); + } + var token = _orgDeleteTokenDataFactory.Protect(new OrgDeleteTokenable(organization, 1)); + await _mailService.SendInitiateDeleteOrganzationEmailAsync(orgAdminEmail, organization, token); + } +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 0495c4c76e..7d73a3c903 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -28,8 +28,6 @@ public interface IOrganizationService /// Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey, string privateKey); - Task InitiateDeleteAsync(Organization organization, string orgAdminEmail); - Task DeleteAsync(Organization organization); Task EnableAsync(Guid organizationId, DateTime? expirationDate); Task DisableAsync(Guid organizationId, DateTime? expirationDate); Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate); diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 8743e51ff2..70a3227a71 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -4,7 +4,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Business; -using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; @@ -68,7 +67,6 @@ public class OrganizationService : IOrganizationService private readonly IProviderUserRepository _providerUserRepository; private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery; private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; - private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; private readonly IProviderRepository _providerRepository; private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; @@ -106,7 +104,6 @@ public class OrganizationService : IOrganizationService IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory, IDataProtectorTokenFactory orgUserInviteTokenDataFactory, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, - IDataProtectorTokenFactory orgDeleteTokenDataFactory, IProviderRepository providerRepository, IFeatureService featureService, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, @@ -139,7 +136,6 @@ public class OrganizationService : IOrganizationService _providerUserRepository = providerUserRepository; _countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery; _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; - _orgDeleteTokenDataFactory = orgDeleteTokenDataFactory; _providerRepository = providerRepository; _orgUserInviteTokenableFactory = orgUserInviteTokenableFactory; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; @@ -690,44 +686,6 @@ public class OrganizationService : IOrganizationService } } - public async Task InitiateDeleteAsync(Organization organization, string orgAdminEmail) - { - var orgAdmin = await _userRepository.GetByEmailAsync(orgAdminEmail); - if (orgAdmin == null) - { - throw new BadRequestException("Org admin not found."); - } - var orgAdminOrgUser = await _organizationUserRepository.GetDetailsByUserAsync(orgAdmin.Id, organization.Id); - if (orgAdminOrgUser == null || orgAdminOrgUser.Status != OrganizationUserStatusType.Confirmed || - (orgAdminOrgUser.Type != OrganizationUserType.Admin && orgAdminOrgUser.Type != OrganizationUserType.Owner)) - { - throw new BadRequestException("Org admin not found."); - } - var token = _orgDeleteTokenDataFactory.Protect(new OrgDeleteTokenable(organization, 1)); - await _mailService.SendInitiateDeleteOrganzationEmailAsync(orgAdminEmail, organization, token); - } - - public async Task DeleteAsync(Organization organization) - { - await ValidateDeleteOrganizationAsync(organization); - - if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) - { - try - { - var eop = !organization.ExpirationDate.HasValue || - organization.ExpirationDate.Value >= DateTime.UtcNow; - await _paymentService.CancelSubscriptionAsync(organization, eop); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.DeleteAccount, organization, _currentContext)); - } - catch (GatewayException) { } - } - - await _organizationRepository.DeleteAsync(organization); - await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id); - } - public async Task EnableAsync(Guid organizationId, DateTime? expirationDate) { var org = await GetOrgById(organizationId); @@ -1978,15 +1936,6 @@ public class OrganizationService : IOrganizationService return true; } - private async Task ValidateDeleteOrganizationAsync(Organization organization) - { - var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id); - if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector) - { - throw new BadRequestException("You cannot delete an Organization that is using Key Connector."); - } - } - public async Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId) { if (revokingUserId.HasValue && organizationUser.UserId == revokingUserId.Value) diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 5586273520..9d2e6e51e6 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -9,6 +9,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfa using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; @@ -52,6 +53,7 @@ public static class OrganizationServiceCollectionExtensions services.AddOrganizationLicenseCommandsQueries(); services.AddOrganizationDomainCommandsQueries(); services.AddOrganizationSignUpCommands(); + services.AddOrganizationDeleteCommands(); services.AddOrganizationAuthCommands(); services.AddOrganizationUserCommands(); services.AddOrganizationUserCommandsQueries(); @@ -61,6 +63,12 @@ public static class OrganizationServiceCollectionExtensions private static IServiceCollection AddOrganizationSignUpCommands(this IServiceCollection services) => services.AddScoped(); + private static void AddOrganizationDeleteCommands(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + } + private static void AddOrganizationConnectionCommands(this IServiceCollection services) { services.AddScoped(); diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index f35dbaa5cf..b739469c78 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; @@ -52,6 +53,7 @@ public class OrganizationsControllerTests : IDisposable private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand; + private readonly IOrganizationDeleteCommand _organizationDeleteCommand; private readonly OrganizationsController _sut; public OrganizationsControllerTests() @@ -75,6 +77,7 @@ public class OrganizationsControllerTests : IDisposable _orgDeleteTokenDataFactory = Substitute.For>(); _removeOrganizationUserCommand = Substitute.For(); _cloudOrganizationSignUpCommand = Substitute.For(); + _organizationDeleteCommand = Substitute.For(); _sut = new OrganizationsController( _organizationRepository, @@ -95,7 +98,8 @@ public class OrganizationsControllerTests : IDisposable _providerBillingService, _orgDeleteTokenDataFactory, _removeOrganizationUserCommand, - _cloudOrganizationSignUpCommand); + _cloudOrganizationSignUpCommand, + _organizationDeleteCommand); } public void Dispose() @@ -226,6 +230,6 @@ public class OrganizationsControllerTests : IDisposable await _providerBillingService.Received(1) .ScaleSeats(provider, organization.PlanType, -organization.Seats.Value); - await _organizationService.Received(1).DeleteAsync(organization); + await _organizationDeleteCommand.Received(1).DeleteAsync(organization); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommandTests.cs new file mode 100644 index 0000000000..0a83bb89d8 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommandTests.cs @@ -0,0 +1,53 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.Repositories; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Test.AutoFixture.OrganizationFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations; + +[SutProviderCustomize] +public class OrganizationDeleteCommandTests +{ + [Theory, PaidOrganizationCustomize, BitAutoData] + public async Task Delete_Success(Organization organization, SutProvider sutProvider) + { + var organizationRepository = sutProvider.GetDependency(); + var applicationCacheService = sutProvider.GetDependency(); + + await sutProvider.Sut.DeleteAsync(organization); + + await organizationRepository.Received().DeleteAsync(organization); + await applicationCacheService.Received().DeleteOrganizationAbilityAsync(organization.Id); + } + + [Theory, PaidOrganizationCustomize, BitAutoData] + public async Task Delete_Fails_KeyConnector(Organization organization, SutProvider sutProvider, + SsoConfig ssoConfig) + { + ssoConfig.Enabled = true; + ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector }); + var ssoConfigRepository = sutProvider.GetDependency(); + var organizationRepository = sutProvider.GetDependency(); + var applicationCacheService = sutProvider.GetDependency(); + + ssoConfigRepository.GetByOrganizationIdAsync(organization.Id).Returns(ssoConfig); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.DeleteAsync(organization)); + + Assert.Contains("You cannot delete an Organization that is using Key Connector.", exception.Message); + + await organizationRepository.DidNotReceiveWithAnyArgs().DeleteAsync(default); + await applicationCacheService.DidNotReceiveWithAnyArgs().DeleteOrganizationAbilityAsync(default); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationInitiateDeleteCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationInitiateDeleteCommandTests.cs new file mode 100644 index 0000000000..41c5b569d4 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationInitiateDeleteCommandTests.cs @@ -0,0 +1,112 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Business.Tokenables; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tokens; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations; + +[SutProviderCustomize] +public class OrganizationInitiateDeleteCommandTests +{ + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task InitiateDeleteAsync_ValidAdminUser_Success(OrganizationUserType organizationUserType, + Organization organization, User orgAdmin, OrganizationUserOrganizationDetails orgAdminUser, + string token, SutProvider sutProvider) + { + orgAdminUser.Type = organizationUserType; + orgAdminUser.Status = OrganizationUserStatusType.Confirmed; + + sutProvider.GetDependency() + .GetByEmailAsync(orgAdmin.Email) + .Returns(orgAdmin); + + sutProvider.GetDependency() + .GetDetailsByUserAsync(orgAdmin.Id, organization.Id) + .Returns(orgAdminUser); + + sutProvider.GetDependency>() + .Protect(Arg.Any()) + .Returns(token); + + await sutProvider.Sut.InitiateDeleteAsync(organization, orgAdmin.Email); + + await sutProvider.GetDependency().Received(1) + .SendInitiateDeleteOrganzationEmailAsync(orgAdmin.Email, organization, token); + } + + [Theory, BitAutoData] + public async Task InitiateDeleteAsync_UserNotFound_ThrowsBadRequest( + Organization organization, string email, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByEmailAsync(email) + .Returns((User)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.InitiateDeleteAsync(organization, email)); + + Assert.Equal(OrganizationInitiateDeleteCommand.OrganizationAdminNotFoundErrorMessage, exception.Message); + } + + [Theory] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task InitiateDeleteAsync_UserNotOrgAdmin_ThrowsBadRequest(OrganizationUserType organizationUserType, + Organization organization, User user, OrganizationUserOrganizationDetails orgUser, + SutProvider sutProvider) + { + orgUser.Type = organizationUserType; + orgUser.Status = OrganizationUserStatusType.Confirmed; + + sutProvider.GetDependency() + .GetByEmailAsync(user.Email) + .Returns(user); + + sutProvider.GetDependency() + .GetDetailsByUserAsync(user.Id, organization.Id) + .Returns(orgUser); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.InitiateDeleteAsync(organization, user.Email)); + + Assert.Equal(OrganizationInitiateDeleteCommand.OrganizationAdminNotFoundErrorMessage, exception.Message); + } + + [Theory] + [BitAutoData(OrganizationUserStatusType.Invited)] + [BitAutoData(OrganizationUserStatusType.Revoked)] + [BitAutoData(OrganizationUserStatusType.Accepted)] + public async Task InitiateDeleteAsync_UserNotConfirmed_ThrowsBadRequest( + OrganizationUserStatusType organizationUserStatusType, + Organization organization, User user, OrganizationUserOrganizationDetails orgUser, + SutProvider sutProvider) + { + orgUser.Type = OrganizationUserType.Admin; + orgUser.Status = organizationUserStatusType; + + sutProvider.GetDependency() + .GetByEmailAsync(user.Email) + .Returns(user); + + sutProvider.GetDependency() + .GetDetailsByUserAsync(user.Id, organization.Id) + .Returns(orgUser); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.InitiateDeleteAsync(organization, user.Email)); + + Assert.Equal(OrganizationInitiateDeleteCommand.OrganizationAdminNotFoundErrorMessage, exception.Message); + } +} diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index 45cab3912c..cd680f2ef0 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -6,9 +6,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; 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.Business.Tokenables; -using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; @@ -1427,39 +1425,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) Assert.Contains("Seat limit has been reached. Contact your provider to purchase additional seats.", failureMessage); } - [Theory, PaidOrganizationCustomize, BitAutoData] - public async Task Delete_Success(Organization organization, SutProvider sutProvider) - { - var organizationRepository = sutProvider.GetDependency(); - var applicationCacheService = sutProvider.GetDependency(); - - await sutProvider.Sut.DeleteAsync(organization); - - await organizationRepository.Received().DeleteAsync(organization); - await applicationCacheService.Received().DeleteOrganizationAbilityAsync(organization.Id); - } - - [Theory, PaidOrganizationCustomize, BitAutoData] - public async Task Delete_Fails_KeyConnector(Organization organization, SutProvider sutProvider, - SsoConfig ssoConfig) - { - ssoConfig.Enabled = true; - ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector }); - var ssoConfigRepository = sutProvider.GetDependency(); - var organizationRepository = sutProvider.GetDependency(); - var applicationCacheService = sutProvider.GetDependency(); - - ssoConfigRepository.GetByOrganizationIdAsync(organization.Id).Returns(ssoConfig); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.DeleteAsync(organization)); - - Assert.Contains("You cannot delete an Organization that is using Key Connector.", exception.Message); - - await organizationRepository.DidNotReceiveWithAnyArgs().DeleteAsync(default); - await applicationCacheService.DidNotReceiveWithAnyArgs().DeleteOrganizationAbilityAsync(default); - } - private void RestoreRevokeUser_Setup( Organization organization, OrganizationUser? requestingOrganizationUser, From 9e718d733655f41514f0a026e7137f67fcedcdbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:59:46 +0000 Subject: [PATCH 057/148] [PM-15637] Add Email Notification Templates and Logic for Device Approval Requests (#5270) * Add device approval notification email templates * Add DeviceApprovalRequestedViewModel for device approval notifications * Add method to send device approval requested notification email * Send email notification to Organization Admins when adding a new admin approval auth request * Add tests for device approval notification email sending in AuthRequestServiceTests * fix(email-templates): Remove unnecessary triple braces from user name variable in device approval notification emails * Add feature flag for admin notifications on device approval requests * Add logging for skipped admin notifications on device approval requests --- .../Mail/DeviceApprovalRequestedViewModel.cs | 14 +++ .../Implementations/AuthRequestService.cs | 29 ++++- src/Core/Constants.cs | 1 + ...otifyAdminDeviceApprovalRequested.html.hbs | 23 ++++ ...otifyAdminDeviceApprovalRequested.text.hbs | 5 + ...otifyAdminDeviceApprovalRequested.html.hbs | 29 +++++ ...otifyAdminDeviceApprovalRequested.text.hbs | 7 ++ src/Core/Services/IMailService.cs | 1 + .../Implementations/HandlebarsMailService.cs | 18 +++ .../NoopImplementations/NoopMailService.cs | 5 + .../Auth/Services/AuthRequestServiceTests.cs | 118 ++++++++++++++++++ 11 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 src/Core/AdminConsole/Models/Mail/DeviceApprovalRequestedViewModel.cs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/NotifyAdminDeviceApprovalRequested.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/NotifyAdminDeviceApprovalRequested.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/SelfHostNotifyAdminDeviceApprovalRequested.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/AdminConsole/SelfHostNotifyAdminDeviceApprovalRequested.text.hbs diff --git a/src/Core/AdminConsole/Models/Mail/DeviceApprovalRequestedViewModel.cs b/src/Core/AdminConsole/Models/Mail/DeviceApprovalRequestedViewModel.cs new file mode 100644 index 0000000000..7f6c932619 --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/DeviceApprovalRequestedViewModel.cs @@ -0,0 +1,14 @@ +using Bit.Core.Models.Mail; + +namespace Bit.Core.AdminConsole.Models.Mail; + +public class DeviceApprovalRequestedViewModel : BaseMailModel +{ + public Guid OrganizationId { get; set; } + public string UserNameRequestingAccess { get; set; } + + public string Url => string.Format("{0}/organizations/{1}/settings/device-approvals", + WebVaultUrl, + OrganizationId); +} + diff --git a/src/Core/Auth/Services/Implementations/AuthRequestService.cs b/src/Core/Auth/Services/Implementations/AuthRequestService.cs index f83c5de1f6..5e41e3a679 100644 --- a/src/Core/Auth/Services/Implementations/AuthRequestService.cs +++ b/src/Core/Auth/Services/Implementations/AuthRequestService.cs @@ -12,6 +12,7 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; +using Microsoft.Extensions.Logging; #nullable enable @@ -27,6 +28,9 @@ public class AuthRequestService : IAuthRequestService private readonly IPushNotificationService _pushNotificationService; private readonly IEventService _eventService; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IMailService _mailService; + private readonly IFeatureService _featureService; + private readonly ILogger _logger; public AuthRequestService( IAuthRequestRepository authRequestRepository, @@ -36,7 +40,10 @@ public class AuthRequestService : IAuthRequestService ICurrentContext currentContext, IPushNotificationService pushNotificationService, IEventService eventService, - IOrganizationUserRepository organizationRepository) + IOrganizationUserRepository organizationRepository, + IMailService mailService, + IFeatureService featureService, + ILogger logger) { _authRequestRepository = authRequestRepository; _userRepository = userRepository; @@ -46,6 +53,9 @@ public class AuthRequestService : IAuthRequestService _pushNotificationService = pushNotificationService; _eventService = eventService; _organizationUserRepository = organizationRepository; + _mailService = mailService; + _featureService = featureService; + _logger = logger; } public async Task GetAuthRequestAsync(Guid id, Guid userId) @@ -132,6 +142,8 @@ public class AuthRequestService : IAuthRequestService { var createdAuthRequest = await CreateAuthRequestAsync(model, user, organizationUser.OrganizationId); firstAuthRequest ??= createdAuthRequest; + + await NotifyAdminsOfDeviceApprovalRequestAsync(organizationUser, user); } // I know this won't be null because I have already validated that at least one organization exists @@ -276,4 +288,19 @@ public class AuthRequestService : IAuthRequestService { return DateTime.UtcNow > savedDate.Add(allowedLifetime); } + + private async Task NotifyAdminsOfDeviceApprovalRequestAsync(OrganizationUser organizationUser, User user) + { + if (!_featureService.IsEnabled(FeatureFlagKeys.DeviceApprovalRequestAdminNotifications)) + { + _logger.LogWarning("Skipped sending device approval notification to admins - feature flag disabled"); + return; + } + + var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync( + organizationUser.OrganizationId, + OrganizationUserType.Admin); + var adminEmails = admins.Select(a => a.Email).Distinct().ToList(); + await _mailService.SendDeviceApprovalRequestedNotificationEmailAsync(adminEmails, organizationUser.OrganizationId, user.Email, user.Name); + } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index dea65b929e..b4ddd73409 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -106,6 +106,7 @@ public static class FeatureFlagKeys public const string AccountDeprovisioning = "pm-10308-account-deprovisioning"; public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; public const string IntegrationPage = "pm-14505-admin-console-integration-page"; + public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications"; public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection"; diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/NotifyAdminDeviceApprovalRequested.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/NotifyAdminDeviceApprovalRequested.html.hbs new file mode 100644 index 0000000000..a54773a15e --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/NotifyAdminDeviceApprovalRequested.html.hbs @@ -0,0 +1,23 @@ +{{#>FullHtmlLayout}} + + + + + + + +
+ {{UserNameRequestingAccess}} has sent a device approval request. Review login requests to allow the member to finish logging in. +
+
+
+ + Review request + +
+
+{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/NotifyAdminDeviceApprovalRequested.text.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/NotifyAdminDeviceApprovalRequested.text.hbs new file mode 100644 index 0000000000..e396546646 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/NotifyAdminDeviceApprovalRequested.text.hbs @@ -0,0 +1,5 @@ +{{#>BasicTextLayout}} +{{UserNameRequestingAccess}} has sent a device approval request. Review login requests to allow the member to finish logging in. + +{{Url}} +{{/BasicTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/SelfHostNotifyAdminDeviceApprovalRequested.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/SelfHostNotifyAdminDeviceApprovalRequested.html.hbs new file mode 100644 index 0000000000..ee7fcf8cad --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/SelfHostNotifyAdminDeviceApprovalRequested.html.hbs @@ -0,0 +1,29 @@ +{{#>FullHtmlLayout}} + + + + + + + + + + +
+ {{UserNameRequestingAccess}} has sent a device approval request. Review login requests to allow the member to finish logging in. +
+ To review requests, log in to your self-hosted instance → navigate to the Admin Console → select Device Approvals +
+
+
+ + Review request + +
+
+{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/SelfHostNotifyAdminDeviceApprovalRequested.text.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/SelfHostNotifyAdminDeviceApprovalRequested.text.hbs new file mode 100644 index 0000000000..e5b412cc87 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/SelfHostNotifyAdminDeviceApprovalRequested.text.hbs @@ -0,0 +1,7 @@ +{{#>BasicTextLayout}} +{{UserNameRequestingAccess}} has sent a device approval request. Review login requests to allow the member to finish logging in. + +To review requests, log in to your self-hosted instance -> navigate to the Admin Console -> select Device Approvals. + +{{Url}} +{{/BasicTextLayout}} diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 09bae38a03..77914c0188 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -96,5 +96,6 @@ public interface IMailService Task SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(string email, string offerAcceptanceDate, string organizationId, string organizationName); Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList); + Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable adminEmails, Guid organizationId, string email, string userName); } diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index c4ca48d3a3..630c5b0bf0 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -2,6 +2,7 @@ using System.Reflection; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Models.Mail; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Mail; using Bit.Core.Billing.Enums; @@ -1168,6 +1169,23 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable adminEmails, Guid organizationId, string email, string userName) + { + var templateName = _globalSettings.SelfHosted ? + "AdminConsole.SelfHostNotifyAdminDeviceApprovalRequested" : + "AdminConsole.NotifyAdminDeviceApprovalRequested"; + var message = CreateDefaultMessage("Review SSO login request for new device", adminEmails); + var model = new DeviceApprovalRequestedViewModel + { + WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, + UserNameRequestingAccess = GetUserIdentifier(email, userName), + OrganizationId = organizationId, + }; + await AddMessageContentAsync(message, templateName, model); + message.Category = "DeviceApprovalRequested"; + await _mailDeliveryService.SendEmailAsync(message); + } + private static string GetUserIdentifier(string email, string userName) { return string.IsNullOrEmpty(userName) ? email : CoreHelpers.SanitizeForEmail(userName, false); diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 0e07436fd1..13914ddd86 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -316,5 +316,10 @@ public class NoopMailService : IMailService return Task.FromResult(0); } public Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList) => Task.CompletedTask; + + public Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable adminEmails, Guid organizationId, string email, string userName) + { + return Task.FromResult(0); + } } diff --git a/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs b/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs index 4e42125dce..3894ac90a8 100644 --- a/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs +++ b/test/Core.Test/Auth/Services/AuthRequestServiceTests.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.OrganizationUsers; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; @@ -227,6 +228,14 @@ public class AuthRequestServiceTests await sutProvider.GetDependency() .Received() .CreateAsync(createdAuthRequest); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendDeviceApprovalRequestedNotificationEmailAsync( + Arg.Any>(), + Arg.Any(), + Arg.Any(), + Arg.Any()); } /// @@ -321,6 +330,115 @@ public class AuthRequestServiceTests await sutProvider.GetDependency() .Received(1) .LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendDeviceApprovalRequestedNotificationEmailAsync( + Arg.Any>(), + Arg.Any(), + Arg.Any(), + Arg.Any()); + } + + [Theory, BitAutoData] + public async Task CreateAuthRequestAsync_AdminApproval_WithAdminNotifications_CreatesForEachOrganization_SendsEmails( + SutProvider sutProvider, + AuthRequestCreateRequestModel createModel, + User user, + OrganizationUser organizationUser1, + OrganizationUserUserDetails admin1, + OrganizationUser organizationUser2, + OrganizationUserUserDetails admin2, + OrganizationUserUserDetails admin3) + { + createModel.Type = AuthRequestType.AdminApproval; + user.Email = createModel.Email; + organizationUser1.UserId = user.Id; + organizationUser2.UserId = user.Id; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.DeviceApprovalRequestAdminNotifications) + .Returns(true); + + sutProvider.GetDependency() + .GetByEmailAsync(user.Email) + .Returns(user); + + sutProvider.GetDependency() + .DeviceType + .Returns(DeviceType.ChromeExtension); + + sutProvider.GetDependency() + .UserId + .Returns(user.Id); + + sutProvider.GetDependency() + .PasswordlessAuth.KnownDevicesOnly + .Returns(false); + + + sutProvider.GetDependency() + .GetManyByUserAsync(user.Id) + .Returns(new List + { + organizationUser1, + organizationUser2, + }); + + sutProvider.GetDependency() + .GetManyByMinimumRoleAsync(organizationUser1.OrganizationId, OrganizationUserType.Admin) + .Returns( + [ + admin1, + ]); + + sutProvider.GetDependency() + .GetManyByMinimumRoleAsync(organizationUser2.OrganizationId, OrganizationUserType.Admin) + .Returns( + [ + admin2, + admin3, + ]); + + sutProvider.GetDependency() + .CreateAsync(Arg.Any()) + .Returns(c => c.ArgAt(0)); + + var authRequest = await sutProvider.Sut.CreateAuthRequestAsync(createModel); + + Assert.Equal(organizationUser1.OrganizationId, authRequest.OrganizationId); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(Arg.Is(o => o.OrganizationId == organizationUser1.OrganizationId)); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(Arg.Is(o => o.OrganizationId == organizationUser2.OrganizationId)); + + await sutProvider.GetDependency() + .Received(2) + .CreateAsync(Arg.Any()); + + await sutProvider.GetDependency() + .Received(1) + .LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval); + + await sutProvider.GetDependency() + .Received(1) + .SendDeviceApprovalRequestedNotificationEmailAsync( + Arg.Is>(emails => emails.Count() == 1 && emails.Contains(admin1.Email)), + organizationUser1.OrganizationId, + user.Email, + user.Name); + + await sutProvider.GetDependency() + .Received(1) + .SendDeviceApprovalRequestedNotificationEmailAsync( + Arg.Is>(emails => emails.Count() == 2 && emails.Contains(admin2.Email) && emails.Contains(admin3.Email)), + organizationUser2.OrganizationId, + user.Email, + user.Name); } /// From c03abafa71e192cc0e443616adcc5bebc4a368eb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 09:08:15 -0500 Subject: [PATCH 058/148] [deps] Billing: Update CsvHelper to v33 (#5181) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bitwarden_license/src/Commercial.Core/Commercial.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitwarden_license/src/Commercial.Core/Commercial.Core.csproj b/bitwarden_license/src/Commercial.Core/Commercial.Core.csproj index 0b97232931..57babb4043 100644 --- a/bitwarden_license/src/Commercial.Core/Commercial.Core.csproj +++ b/bitwarden_license/src/Commercial.Core/Commercial.Core.csproj @@ -5,7 +5,7 @@ - + From 5310f63514979cd6f4bc607b7cd2e6857ede7da3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 09:17:18 -0500 Subject: [PATCH 059/148] [deps] Billing: Update coverlet.collector to 6.0.4 (#5219) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Alex Morask <144709477+amorask-bitwarden@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 82a92989d1..65041c3023 100644 --- a/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj +++ b/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj @@ -16,7 +16,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 5562ca9d5e2b43e428b47365918d1cb6145baee7 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Mon, 27 Jan 2025 15:28:47 +0100 Subject: [PATCH 060/148] WIP (#5210) --- .../Implementations/UpcomingInvoiceHandler.cs | 8 +-- .../OrganizationBillingService.cs | 2 +- .../PremiumUserBillingService.cs | 2 +- .../Implementations/SubscriberService.cs | 20 ++---- .../Implementations/StripePaymentService.cs | 70 ++++++------------- 5 files changed, 30 insertions(+), 72 deletions(-) diff --git a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs index bd496c6974..c52c03b6aa 100644 --- a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs +++ b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs @@ -1,7 +1,6 @@ using Bit.Billing.Constants; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing.Constants; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -161,18 +160,13 @@ public class UpcomingInvoiceHandler : IUpcomingInvoiceHandler private async Task TryEnableAutomaticTaxAsync(Subscription subscription) { - var customerGetOptions = new CustomerGetOptions { Expand = ["tax"] }; - var customer = await _stripeFacade.GetCustomer(subscription.CustomerId, customerGetOptions); - - if (subscription.AutomaticTax.Enabled || - customer.Tax?.AutomaticTax != StripeConstants.AutomaticTaxStatus.Supported) + if (subscription.AutomaticTax.Enabled) { return subscription; } var subscriptionUpdateOptions = new SubscriptionUpdateOptions { - DefaultTaxRates = [], AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } }; diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index ec9770c59e..201de22525 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -360,7 +360,7 @@ public class OrganizationBillingService( { AutomaticTax = new SubscriptionAutomaticTaxOptions { - Enabled = customer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported + Enabled = true }, CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, Customer = customer.Id, diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index 306ee88eaf..0672a8d5e7 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -235,7 +235,7 @@ public class PremiumUserBillingService( { AutomaticTax = new SubscriptionAutomaticTaxOptions { - Enabled = customer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported, + Enabled = true }, CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, Customer = customer.Id, diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index b2dca19e80..f4cf22ac19 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -661,21 +661,11 @@ public class SubscriberService( } } - if (SubscriberIsEligibleForAutomaticTax(subscriber, customer)) - { - await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId, - new SubscriptionUpdateOptions - { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } - }); - } - - return; - - bool SubscriberIsEligibleForAutomaticTax(ISubscriber localSubscriber, Customer localCustomer) - => !string.IsNullOrEmpty(localSubscriber.GatewaySubscriptionId) && - (localCustomer.Subscriptions?.Any(sub => sub.Id == localSubscriber.GatewaySubscriptionId && !sub.AutomaticTax.Enabled) ?? false) && - localCustomer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported; + await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId, + new SubscriptionUpdateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, + }); } public async Task VerifyBankAccount( diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 3f9c5c53c6..fb5c7364a5 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -177,11 +177,7 @@ public class StripePaymentService : IPaymentService customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions); subCreateOptions.AddExpand("latest_invoice.payment_intent"); subCreateOptions.Customer = customer.Id; - - if (CustomerHasTaxLocationVerified(customer)) - { - subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; - } + subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions); if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null) @@ -362,13 +358,10 @@ public class StripePaymentService : IPaymentService customer = await _stripeAdapter.CustomerUpdateAsync(org.GatewayCustomerId, customerUpdateOptions); } - var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade); - - if (CustomerHasTaxLocationVerified(customer)) + var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade) { - subCreateOptions.DefaultTaxRates = []; - subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; - } + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } + }; var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions); @@ -527,6 +520,10 @@ public class StripePaymentService : IPaymentService var customerCreateOptions = new CustomerCreateOptions { + Tax = new CustomerTaxOptions + { + ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately + }, Description = user.Name, Email = user.Email, Metadata = stripeCustomerMetadata, @@ -564,6 +561,7 @@ public class StripePaymentService : IPaymentService var subCreateOptions = new SubscriptionCreateOptions { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, Customer = customer.Id, Items = [], Metadata = new Dictionary @@ -583,16 +581,10 @@ public class StripePaymentService : IPaymentService subCreateOptions.Items.Add(new SubscriptionItemOptions { Plan = StoragePlanId, - Quantity = additionalStorageGb + Quantity = additionalStorageGb, }); } - if (CustomerHasTaxLocationVerified(customer)) - { - subCreateOptions.DefaultTaxRates = []; - subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; - } - var subscription = await ChargeForNewSubscriptionAsync(user, customer, createdStripeCustomer, stripePaymentMethod, paymentMethodType, subCreateOptions, braintreeCustomer); @@ -630,10 +622,7 @@ public class StripePaymentService : IPaymentService SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items) }); - if (CustomerHasTaxLocationVerified(customer)) - { - previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true }; - } + previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true }; if (previewInvoice.AmountDue > 0) { @@ -691,14 +680,12 @@ public class StripePaymentService : IPaymentService Customer = customer.Id, SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items), SubscriptionDefaultTaxRates = subCreateOptions.DefaultTaxRates, + AutomaticTax = new InvoiceAutomaticTaxOptions + { + Enabled = true + } }; - if (CustomerHasTaxLocationVerified(customer)) - { - upcomingInvoiceOptions.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }; - upcomingInvoiceOptions.SubscriptionDefaultTaxRates = []; - } - var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions); if (previewInvoice.AmountDue > 0) @@ -817,7 +804,11 @@ public class StripePaymentService : IPaymentService Items = updatedItemOptions, ProrationBehavior = invoiceNow ? Constants.AlwaysInvoice : Constants.CreateProrations, DaysUntilDue = daysUntilDue ?? 1, - CollectionMethod = "send_invoice" + CollectionMethod = "send_invoice", + AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = true + } }; if (!invoiceNow && isAnnualPlan && sub.Status.Trim() != "trialing") { @@ -825,13 +816,6 @@ public class StripePaymentService : IPaymentService new SubscriptionPendingInvoiceItemIntervalOptions { Interval = "month" }; } - if (sub.AutomaticTax.Enabled != true && - CustomerHasTaxLocationVerified(sub.Customer)) - { - subUpdateOptions.DefaultTaxRates = []; - subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; - } - if (!subscriptionUpdate.UpdateNeeded(sub)) { // No need to update subscription, quantity matches @@ -1516,13 +1500,11 @@ public class StripePaymentService : IPaymentService if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId) && customer.Subscriptions.Any(sub => sub.Id == subscriber.GatewaySubscriptionId && - !sub.AutomaticTax.Enabled) && - CustomerHasTaxLocationVerified(customer)) + !sub.AutomaticTax.Enabled)) { var subscriptionUpdateOptions = new SubscriptionUpdateOptions { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, - DefaultTaxRates = [] + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } }; _ = await _stripeAdapter.SubscriptionUpdateAsync( @@ -2280,14 +2262,6 @@ public class StripePaymentService : IPaymentService } } - /// - /// Determines if a Stripe customer supports automatic tax - /// - /// - /// - private static bool CustomerHasTaxLocationVerified(Customer customer) => - customer?.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported; - // We are taking only first 30 characters of the SubscriberName because stripe provide // for 30 characters for custom_fields,see the link: https://stripe.com/docs/api/invoices/create private static string GetFirstThirtyCharacters(string subscriberName) From 411291b782966fe17f4e53cd9350d137dac5d7ae Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 27 Jan 2025 15:48:47 +0000 Subject: [PATCH 061/148] Bumped version to 2025.1.5 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 9c54e35e6e..d109303a58 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.1.4 + 2025.1.5 Bit.$(MSBuildProjectName) enable From ec1cf31d9102e5e2366ade65954587dfd2cf2fec Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Mon, 27 Jan 2025 17:20:40 +0100 Subject: [PATCH 062/148] [PM-17425] Cannot open Stripe links for individual premium accounts (#5314) --- src/Admin/Views/Users/Edit.cshtml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Admin/Views/Users/Edit.cshtml b/src/Admin/Views/Users/Edit.cshtml index 04e95c1400..8169b72b3c 100644 --- a/src/Admin/Views/Users/Edit.cshtml +++ b/src/Admin/Views/Users/Edit.cshtml @@ -37,11 +37,7 @@ // Premium document.getElementById('@(nameof(Model.MaxStorageGb))').value = '1'; document.getElementById('@(nameof(Model.Premium))').checked = true; - using Stripe.Entitlements; // Licensing - using Bit.Core; - using Stripe.Entitlements; - using Microsoft.Identity.Client.Extensibility; document.getElementById('@(nameof(Model.LicenseKey))').value = '@Model.RandomLicenseKey'; document.getElementById('@(nameof(Model.PremiumExpirationDate))').value = '@Model.OneYearExpirationDate'; From a51c7a1a8bee339eea8a8c6c6c420ea5f643e708 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Mon, 27 Jan 2025 19:22:55 +0100 Subject: [PATCH 063/148] [BEEEP] Remove unused code (#5320) --- .../Controllers/FreshdeskController.cs | 3 -- src/Billing/Controllers/LoginController.cs | 53 ------------------- 2 files changed, 56 deletions(-) delete mode 100644 src/Billing/Controllers/LoginController.cs diff --git a/src/Billing/Controllers/FreshdeskController.cs b/src/Billing/Controllers/FreshdeskController.cs index 1b6ddea429..7aeb60a67f 100644 --- a/src/Billing/Controllers/FreshdeskController.cs +++ b/src/Billing/Controllers/FreshdeskController.cs @@ -17,7 +17,6 @@ public class FreshdeskController : Controller private readonly BillingSettings _billingSettings; private readonly IUserRepository _userRepository; private readonly IOrganizationRepository _organizationRepository; - private readonly IOrganizationUserRepository _organizationUserRepository; private readonly ILogger _logger; private readonly GlobalSettings _globalSettings; private readonly IHttpClientFactory _httpClientFactory; @@ -25,7 +24,6 @@ public class FreshdeskController : Controller public FreshdeskController( IUserRepository userRepository, IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, IOptions billingSettings, ILogger logger, GlobalSettings globalSettings, @@ -34,7 +32,6 @@ public class FreshdeskController : Controller _billingSettings = billingSettings?.Value; _userRepository = userRepository; _organizationRepository = organizationRepository; - _organizationUserRepository = organizationUserRepository; _logger = logger; _globalSettings = globalSettings; _httpClientFactory = httpClientFactory; diff --git a/src/Billing/Controllers/LoginController.cs b/src/Billing/Controllers/LoginController.cs deleted file mode 100644 index c2df41b92c..0000000000 --- a/src/Billing/Controllers/LoginController.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace Billing.Controllers; - -public class LoginController : Controller -{ - /* - private readonly PasswordlessSignInManager _signInManager; - - public LoginController( - PasswordlessSignInManager signInManager) - { - _signInManager = signInManager; - } - - public IActionResult Index() - { - return View(); - } - - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Index(LoginModel model) - { - if (ModelState.IsValid) - { - var result = await _signInManager.PasswordlessSignInAsync(model.Email, - Url.Action("Confirm", "Login", null, Request.Scheme)); - if (result.Succeeded) - { - return RedirectToAction("Index", "Home"); - } - else - { - ModelState.AddModelError(string.Empty, "Account not found."); - } - } - - return View(model); - } - - public async Task Confirm(string email, string token) - { - var result = await _signInManager.PasswordlessSignInAsync(email, token, false); - if (!result.Succeeded) - { - return View("Error"); - } - - return RedirectToAction("Index", "Home"); - } - */ -} From a9a12301afaa4eaad39b90bbdc35574746480d68 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Mon, 27 Jan 2025 17:01:28 -0500 Subject: [PATCH 064/148] [PM-17120] add feature flag (#5329) * add feature flag * cleanup * cleanup --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index b4ddd73409..848a8805b7 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -166,6 +166,7 @@ public static class FeatureFlagKeys public const string RecordInstallationLastActivityDate = "installation-last-activity-date"; public const string EnablePasswordManagerSyncAndroid = "enable-password-manager-sync-android"; public const string EnablePasswordManagerSynciOS = "enable-password-manager-sync-ios"; + public const string AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner"; public static List GetAllKeys() { From 4e1e514e836e63057a2e20221d55fe483359f36b Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 28 Jan 2025 09:49:51 -0600 Subject: [PATCH 065/148] [PM-11249] Update cipher revision date when an attachment is added or deleted (#4873) * update the cipher revision date when an attachment is added or deleted * store the updated cipher in the DB when an attachment is altered * return cipher from delete attachment endpoint --- .../Vault/Controllers/CiphersController.cs | 4 ++-- .../Data/DeleteAttachmentReponseData.cs | 13 +++++++++++ src/Core/Vault/Services/ICipherService.cs | 2 +- .../Services/Implementations/CipherService.cs | 23 +++++++++++++++---- 4 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 src/Core/Vault/Models/Data/DeleteAttachmentReponseData.cs diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 59984683e5..c8ebb8c402 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -1097,7 +1097,7 @@ public class CiphersController : Controller [HttpDelete("{id}/attachment/{attachmentId}")] [HttpPost("{id}/attachment/{attachmentId}/delete")] - public async Task DeleteAttachment(Guid id, string attachmentId) + public async Task DeleteAttachment(Guid id, string attachmentId) { var userId = _userService.GetProperUserId(User).Value; var cipher = await GetByIdAsync(id, userId); @@ -1106,7 +1106,7 @@ public class CiphersController : Controller throw new NotFoundException(); } - await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, false); + return await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, false); } [HttpDelete("{id}/attachment/{attachmentId}/admin")] diff --git a/src/Core/Vault/Models/Data/DeleteAttachmentReponseData.cs b/src/Core/Vault/Models/Data/DeleteAttachmentReponseData.cs new file mode 100644 index 0000000000..0a5e755572 --- /dev/null +++ b/src/Core/Vault/Models/Data/DeleteAttachmentReponseData.cs @@ -0,0 +1,13 @@ +using Bit.Core.Vault.Entities; + +namespace Bit.Core.Vault.Models.Data; + +public class DeleteAttachmentResponseData +{ + public Cipher Cipher { get; set; } + + public DeleteAttachmentResponseData(Cipher cipher) + { + Cipher = cipher; + } +} diff --git a/src/Core/Vault/Services/ICipherService.cs b/src/Core/Vault/Services/ICipherService.cs index b332fbb96f..17f55cb47d 100644 --- a/src/Core/Vault/Services/ICipherService.cs +++ b/src/Core/Vault/Services/ICipherService.cs @@ -17,7 +17,7 @@ public interface ICipherService string attachmentId, Guid organizationShareId); Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false); Task DeleteManyAsync(IEnumerable cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false); - Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false); + Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false); Task PurgeAsync(Guid organizationId); Task MoveManyAsync(IEnumerable cipherIds, Guid? destinationFolderId, Guid movingUserId); Task SaveFolderAsync(Folder folder); diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index ea4db01fd7..90c03df90b 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -210,6 +210,11 @@ public class CipherService : ICipherService AttachmentData = JsonSerializer.Serialize(data) }); cipher.AddAttachment(attachmentId, data); + + // Update the revision date when an attachment is added + cipher.RevisionDate = DateTime.UtcNow; + await _cipherRepository.ReplaceAsync((CipherDetails)cipher); + await _pushService.PushSyncCipherUpdateAsync(cipher, null); return (attachmentId, uploadUrl); @@ -259,6 +264,10 @@ public class CipherService : ICipherService throw; } + // Update the revision date when an attachment is added + cipher.RevisionDate = DateTime.UtcNow; + await _cipherRepository.ReplaceAsync((CipherDetails)cipher); + // push await _pushService.PushSyncCipherUpdateAsync(cipher, null); } @@ -441,7 +450,7 @@ public class CipherService : ICipherService await _pushService.PushSyncCiphersAsync(deletingUserId); } - public async Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, + public async Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false) { if (!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId))) @@ -454,7 +463,7 @@ public class CipherService : ICipherService throw new NotFoundException(); } - await DeleteAttachmentAsync(cipher, cipher.GetAttachments()[attachmentId]); + return await DeleteAttachmentAsync(cipher, cipher.GetAttachments()[attachmentId]); } public async Task PurgeAsync(Guid organizationId) @@ -834,11 +843,11 @@ public class CipherService : ICipherService } } - private async Task DeleteAttachmentAsync(Cipher cipher, CipherAttachment.MetaData attachmentData) + private async Task DeleteAttachmentAsync(Cipher cipher, CipherAttachment.MetaData attachmentData) { if (attachmentData == null || string.IsNullOrWhiteSpace(attachmentData.AttachmentId)) { - return; + return null; } await _cipherRepository.DeleteAttachmentAsync(cipher.Id, attachmentData.AttachmentId); @@ -846,8 +855,14 @@ public class CipherService : ICipherService await _attachmentStorageService.DeleteAttachmentAsync(cipher.Id, attachmentData); await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_AttachmentDeleted); + // Update the revision date when an attachment is deleted + cipher.RevisionDate = DateTime.UtcNow; + await _cipherRepository.ReplaceAsync((CipherDetails)cipher); + // push await _pushService.PushSyncCipherUpdateAsync(cipher, null); + + return new DeleteAttachmentResponseData(cipher); } private async Task ValidateCipherEditForAttachmentAsync(Cipher cipher, Guid savingUserId, bool orgAdmin, From 6d7bdb6ec000f3f2c795a9b9b8f658b4fafea7bc Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Tue, 28 Jan 2025 12:23:15 -0500 Subject: [PATCH 066/148] Ac/pm 17217/add use policy check for accept endpoint(#5324) --- .../OrganizationUsersController.cs | 23 ++++++-- .../OrganizationUsersControllerTests.cs | 58 +++++++++++++++++-- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 12d11fbc18..265aefc4ca 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -311,10 +311,8 @@ public class OrganizationUsersController : Controller throw new UnauthorizedAccessException(); } - var masterPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword); - var useMasterPasswordPolicy = masterPasswordPolicy != null && - masterPasswordPolicy.Enabled && - masterPasswordPolicy.GetDataModel().AutoEnrollEnabled; + var useMasterPasswordPolicy = await ShouldHandleResetPasswordAsync(orgId); + if (useMasterPasswordPolicy && string.IsNullOrWhiteSpace(model.ResetPasswordKey)) { throw new BadRequestException(string.Empty, "Master Password reset is required, but not provided."); @@ -328,6 +326,23 @@ public class OrganizationUsersController : Controller } } + private async Task ShouldHandleResetPasswordAsync(Guid orgId) + { + var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId); + + if (organizationAbility is not { UsePolicies: true }) + { + return false; + } + + var masterPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword); + var useMasterPasswordPolicy = masterPasswordPolicy != null && + masterPasswordPolicy.Enabled && + masterPasswordPolicy.GetDataModel().AutoEnrollEnabled; + + return useMasterPasswordPolicy; + } + [HttpPost("{id}/confirm")] public async Task Confirm(string orgId, string id, [FromBody] OrganizationUserConfirmRequestModel model) { diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs index 0ba8a101d7..e3071bd227 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs @@ -123,24 +123,74 @@ public class OrganizationUsersControllerTests [Theory] [BitAutoData] - public async Task Accept_RequireMasterPasswordReset(Guid orgId, Guid orgUserId, + public async Task Accept_WhenOrganizationUsePoliciesIsEnabledAndResetPolicyIsEnabled_ShouldHandleResetPassword(Guid orgId, Guid orgUserId, OrganizationUserAcceptRequestModel model, User user, SutProvider sutProvider) { + // Arrange + var applicationCacheService = sutProvider.GetDependency(); + applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = true }); + var policy = new Policy { Enabled = true, Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }), }; - sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user); - sutProvider.GetDependency().GetByOrganizationIdTypeAsync(orgId, + var userService = sutProvider.GetDependency(); + userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user); + + + var policyRepository = sutProvider.GetDependency(); + policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword).Returns(policy); + // Act await sutProvider.Sut.Accept(orgId, orgUserId, model); + // Assert await sutProvider.GetDependency().Received(1) - .AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, sutProvider.GetDependency()); + .AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService); await sutProvider.GetDependency().Received(1) .UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id); + + await userService.Received(1).GetUserByPrincipalAsync(default); + await applicationCacheService.Received(1).GetOrganizationAbilityAsync(orgId); + await policyRepository.Received(1).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword); + + } + + [Theory] + [BitAutoData] + public async Task Accept_WhenOrganizationUsePoliciesIsDisabled_ShouldNotHandleResetPassword(Guid orgId, Guid orgUserId, + OrganizationUserAcceptRequestModel model, User user, SutProvider sutProvider) + { + // Arrange + var applicationCacheService = sutProvider.GetDependency(); + applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = false }); + + var policy = new Policy + { + Enabled = true, + Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }), + }; + var userService = sutProvider.GetDependency(); + userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user); + + var policyRepository = sutProvider.GetDependency(); + policyRepository.GetByOrganizationIdTypeAsync(orgId, + PolicyType.ResetPassword).Returns(policy); + + // Act + await sutProvider.Sut.Accept(orgId, orgUserId, model); + + // Assert + await userService.Received(1).GetUserByPrincipalAsync(default); + await sutProvider.GetDependency().Received(1) + .AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService); + await sutProvider.GetDependency().Received(0) + .UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id); + + await policyRepository.Received(0).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword); + await applicationCacheService.Received(1).GetOrganizationAbilityAsync(orgId); } [Theory] From 93f5b34223d1f6648ce75bd8180240dc06949b41 Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Tue, 28 Jan 2025 12:58:04 -0500 Subject: [PATCH 067/148] Add limit item deletion server code (#5308) --- .../Models/Response/Organizations/OrganizationResponseModel.cs | 2 ++ .../Models/Response/ProfileOrganizationResponseModel.cs | 2 ++ .../Response/ProfileProviderOrganizationResponseModel.cs | 1 + .../OrganizationCollectionManagementUpdateRequestModel.cs | 2 ++ .../Models/Data/Organizations/OrganizationAbility.cs | 2 ++ .../OrganizationUsers/OrganizationUserOrganizationDetails.cs | 1 + .../Models/Data/Organizations/SelfHostedOrganizationDetails.cs | 1 + .../Models/Data/Provider/ProviderUserOrganizationDetails.cs | 1 + src/Core/Models/PushNotification.cs | 1 + .../NotificationHub/NotificationHubPushNotificationService.cs | 3 ++- .../Push/Services/AzureQueuePushNotificationService.cs | 3 ++- .../Push/Services/NotificationsApiPushNotificationService.cs | 3 ++- .../Platform/Push/Services/RelayPushNotificationService.cs | 3 ++- .../AdminConsole/Repositories/OrganizationRepository.cs | 1 + .../Queries/OrganizationUserOrganizationDetailsViewQuery.cs | 1 + .../Queries/ProviderUserOrganizationDetailsViewQuery.cs | 1 + 16 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs index 116b4b1238..272aaf6f9c 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs @@ -57,6 +57,7 @@ public class OrganizationResponseModel : ResponseModel MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts; LimitCollectionCreation = organization.LimitCollectionCreation; LimitCollectionDeletion = organization.LimitCollectionDeletion; + LimitItemDeletion = organization.LimitItemDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; UseRiskInsights = organization.UseRiskInsights; } @@ -102,6 +103,7 @@ public class OrganizationResponseModel : ResponseModel public int? MaxAutoscaleSmServiceAccounts { get; set; } public bool LimitCollectionCreation { get; set; } public bool LimitCollectionDeletion { get; set; } + public bool LimitItemDeletion { 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 75e4c44a6d..d08298de6e 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs @@ -67,6 +67,7 @@ public class ProfileOrganizationResponseModel : ResponseModel AccessSecretsManager = organization.AccessSecretsManager; LimitCollectionCreation = organization.LimitCollectionCreation; LimitCollectionDeletion = organization.LimitCollectionDeletion; + LimitItemDeletion = organization.LimitItemDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId); UseRiskInsights = organization.UseRiskInsights; @@ -128,6 +129,7 @@ public class ProfileOrganizationResponseModel : ResponseModel public bool AccessSecretsManager { get; set; } public bool LimitCollectionCreation { get; set; } public bool LimitCollectionDeletion { get; set; } + public bool LimitItemDeletion { 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 211476dca1..589744c7df 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs @@ -47,6 +47,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier; LimitCollectionCreation = organization.LimitCollectionCreation; LimitCollectionDeletion = organization.LimitCollectionDeletion; + LimitItemDeletion = organization.LimitItemDeletion; 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 94f842ca1e..829840c896 100644 --- a/src/Api/Models/Request/Organizations/OrganizationCollectionManagementUpdateRequestModel.cs +++ b/src/Api/Models/Request/Organizations/OrganizationCollectionManagementUpdateRequestModel.cs @@ -7,12 +7,14 @@ public class OrganizationCollectionManagementUpdateRequestModel { public bool LimitCollectionCreation { get; set; } public bool LimitCollectionDeletion { get; set; } + public bool LimitItemDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } public virtual Organization ToOrganization(Organization existingOrganization, IFeatureService featureService) { existingOrganization.LimitCollectionCreation = LimitCollectionCreation; existingOrganization.LimitCollectionDeletion = LimitCollectionDeletion; + existingOrganization.LimitItemDeletion = LimitItemDeletion; existingOrganization.AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems; return existingOrganization; } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs index 6392e483ce..62914f6fa8 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs @@ -23,6 +23,7 @@ public class OrganizationAbility UsePolicies = organization.UsePolicies; LimitCollectionCreation = organization.LimitCollectionCreation; LimitCollectionDeletion = organization.LimitCollectionDeletion; + LimitItemDeletion = organization.LimitItemDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; UseRiskInsights = organization.UseRiskInsights; } @@ -41,6 +42,7 @@ public class OrganizationAbility public bool UsePolicies { get; set; } public bool LimitCollectionCreation { get; set; } public bool LimitCollectionDeletion { get; set; } + public bool LimitItemDeletion { 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 e06b6bd66a..18d68af220 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs @@ -56,6 +56,7 @@ public class OrganizationUserOrganizationDetails public int? SmServiceAccounts { get; set; } public bool LimitCollectionCreation { get; set; } public bool LimitCollectionDeletion { get; set; } + public bool LimitItemDeletion { 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 bd727f707b..c53ac8745c 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs @@ -146,6 +146,7 @@ public class SelfHostedOrganizationDetails : Organization OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling, LimitCollectionCreation = LimitCollectionCreation, LimitCollectionDeletion = LimitCollectionDeletion, + LimitItemDeletion = LimitItemDeletion, 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 bd5592edfc..57f176666a 100644 --- a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs @@ -42,6 +42,7 @@ public class ProviderUserOrganizationDetails public PlanType PlanType { get; set; } public bool LimitCollectionCreation { get; set; } public bool LimitCollectionDeletion { get; set; } + public bool LimitItemDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool UseRiskInsights { get; set; } public ProviderType ProviderType { get; set; } diff --git a/src/Core/Models/PushNotification.cs b/src/Core/Models/PushNotification.cs index 9907abcb65..e2247881ea 100644 --- a/src/Core/Models/PushNotification.cs +++ b/src/Core/Models/PushNotification.cs @@ -62,4 +62,5 @@ public class OrganizationCollectionManagementPushNotification public Guid OrganizationId { get; init; } public bool LimitCollectionCreation { get; init; } public bool LimitCollectionDeletion { get; init; } + public bool LimitItemDeletion { get; init; } } diff --git a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs index d90ebdd744..d99cbf3fe7 100644 --- a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs @@ -246,7 +246,8 @@ public class NotificationHubPushNotificationService : IPushNotificationService { OrganizationId = organization.Id, LimitCollectionCreation = organization.LimitCollectionCreation, - LimitCollectionDeletion = organization.LimitCollectionDeletion + LimitCollectionDeletion = organization.LimitCollectionDeletion, + LimitItemDeletion = organization.LimitItemDeletion }, false ); diff --git a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs index 0503a22a60..33272ce870 100644 --- a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs +++ b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs @@ -239,6 +239,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService { OrganizationId = organization.Id, LimitCollectionCreation = organization.LimitCollectionCreation, - LimitCollectionDeletion = organization.LimitCollectionDeletion + LimitCollectionDeletion = organization.LimitCollectionDeletion, + LimitItemDeletion = organization.LimitItemDeletion }, false); } diff --git a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs index 849ae1b765..5ebfc811ef 100644 --- a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs @@ -248,6 +248,7 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService { OrganizationId = organization.Id, LimitCollectionCreation = organization.LimitCollectionCreation, - LimitCollectionDeletion = organization.LimitCollectionDeletion + LimitCollectionDeletion = organization.LimitCollectionDeletion, + LimitItemDeletion = organization.LimitItemDeletion }, false); } diff --git a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs index e41244a1b8..6549ab47c3 100644 --- a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs @@ -273,7 +273,8 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti { OrganizationId = organization.Id, LimitCollectionCreation = organization.LimitCollectionCreation, - LimitCollectionDeletion = organization.LimitCollectionDeletion + LimitCollectionDeletion = organization.LimitCollectionDeletion, + LimitItemDeletion = organization.LimitItemDeletion }, false ); diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index fb3766c6c7..c1c78eee60 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -101,6 +101,7 @@ public class OrganizationRepository : Repository Date: Tue, 28 Jan 2025 13:39:19 -0500 Subject: [PATCH 068/148] [PM-15906] Add feature flags for Android single tap passkey flows (#5334) Add feature flags to control single tap passkey creation and authentication in the Android client. --- src/Core/Constants.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 848a8805b7..6f08ee4f6b 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -167,6 +167,8 @@ public static class FeatureFlagKeys public const string EnablePasswordManagerSyncAndroid = "enable-password-manager-sync-android"; public const string EnablePasswordManagerSynciOS = "enable-password-manager-sync-ios"; public const string AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner"; + public const string SingleTapPasskeyCreation = "single-tap-passkey-creation"; + public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication"; public static List GetAllKeys() { From f1c94a1400137e150ef1b999448a2e6476cbf1b5 Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:52:11 -0500 Subject: [PATCH 069/148] Risk insights feature flag for server (#5328) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 6f08ee4f6b..6d70f0b3ce 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -169,6 +169,7 @@ public static class FeatureFlagKeys public const string AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner"; public const string SingleTapPasskeyCreation = "single-tap-passkey-creation"; public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication"; + public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications"; public static List GetAllKeys() { From 62afa0b30af772f1e644abe231608228e81c3520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:13:36 +0000 Subject: [PATCH 070/148] [PM-17691] Change permission requirement for organization deletion initiation (#5339) --- src/Admin/AdminConsole/Controllers/OrganizationsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index b24226ee35..3fdef169b4 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -309,7 +309,7 @@ public class OrganizationsController : Controller [HttpPost] [ValidateAntiForgeryToken] - [RequirePermission(Permission.Org_Delete)] + [RequirePermission(Permission.Org_RequestDelete)] public async Task DeleteInitiation(Guid id, OrganizationInitiateDeleteModel model) { if (!ModelState.IsValid) From a5b3f80d7124c8d243d41a528397501ec77b2bfd Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:08:29 -0500 Subject: [PATCH 071/148] [PM-16053] Add DeviceType enum to AuthRequest response model (#5341) --- src/Api/Auth/Models/Response/AuthRequestResponseModel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs b/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs index 0234fc333a..3a07873451 100644 --- a/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs +++ b/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Reflection; using Bit.Core.Auth.Entities; +using Bit.Core.Enums; using Bit.Core.Models.Api; namespace Bit.Api.Auth.Models.Response; @@ -17,6 +18,7 @@ public class AuthRequestResponseModel : ResponseModel Id = authRequest.Id; PublicKey = authRequest.PublicKey; + RequestDeviceTypeValue = authRequest.RequestDeviceType; RequestDeviceType = authRequest.RequestDeviceType.GetType().GetMember(authRequest.RequestDeviceType.ToString()) .FirstOrDefault()?.GetCustomAttribute()?.GetName(); RequestIpAddress = authRequest.RequestIpAddress; @@ -30,6 +32,7 @@ public class AuthRequestResponseModel : ResponseModel public Guid Id { get; set; } public string PublicKey { get; set; } + public DeviceType RequestDeviceTypeValue { get; set; } public string RequestDeviceType { get; set; } public string RequestIpAddress { get; set; } public string Key { get; set; } From 2f2ef20c749478f643b1aff925b48ff8bf5a0389 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 29 Jan 2025 12:07:03 -0800 Subject: [PATCH 072/148] Add missing IGetTasksForOrganizationQuery query registration (#5343) --- src/Core/Vault/VaultServiceCollectionExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Vault/VaultServiceCollectionExtensions.cs b/src/Core/Vault/VaultServiceCollectionExtensions.cs index 4995d0405f..169a62d12d 100644 --- a/src/Core/Vault/VaultServiceCollectionExtensions.cs +++ b/src/Core/Vault/VaultServiceCollectionExtensions.cs @@ -20,5 +20,6 @@ public static class VaultServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } From ad2ea4ca21a7ca770fd04b123638d6b8c8578dc8 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:26:34 -0500 Subject: [PATCH 073/148] Don't enable tax for customer without tax info (#5347) --- .../Implementations/OrganizationBillingService.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index 201de22525..1bc4f792d7 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -356,11 +356,20 @@ public class OrganizationBillingService( } } + var customerHasTaxInfo = customer is + { + Address: + { + Country: not null and not "", + PostalCode: not null and not "" + } + }; + var subscriptionCreateOptions = new SubscriptionCreateOptions { AutomaticTax = new SubscriptionAutomaticTaxOptions { - Enabled = true + Enabled = customerHasTaxInfo }, CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, Customer = customer.Id, From 23dce5810326b5a307ebdbd6bc09528964de5222 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:58:14 -0500 Subject: [PATCH 074/148] [deps] Billing: Update xunit to 2.9.3 (#5289) Co-authored-by: renovate[bot] <29139614+renovate[bot]@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 65041c3023..1ba67ba61e 100644 --- a/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj +++ b/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj @@ -11,7 +11,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 443a147433d30c45ddff1f8b80e626e890253197 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:55:05 -0500 Subject: [PATCH 075/148] Replace StripePaymentService with PremiumUserBillingService in ReplacePaymentMethodAsync call (#5350) --- .../Services/IPremiumUserBillingService.cs | 8 ++++++- .../PremiumUserBillingService.cs | 23 +++++++++++++++++++ .../Services/Implementations/UserService.cs | 11 +++++---- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/Core/Billing/Services/IPremiumUserBillingService.cs b/src/Core/Billing/Services/IPremiumUserBillingService.cs index f74bf6c8da..2161b247b9 100644 --- a/src/Core/Billing/Services/IPremiumUserBillingService.cs +++ b/src/Core/Billing/Services/IPremiumUserBillingService.cs @@ -1,4 +1,5 @@ -using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Models; +using Bit.Core.Billing.Models.Sales; using Bit.Core.Entities; namespace Bit.Core.Billing.Services; @@ -27,4 +28,9 @@ public interface IPremiumUserBillingService /// /// Task Finalize(PremiumUserSale sale); + + Task UpdatePaymentMethod( + User user, + TokenizedPaymentSource tokenizedPaymentSource, + TaxInformation taxInformation); } diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index 0672a8d5e7..ed841c9576 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -1,5 +1,6 @@ using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; using Bit.Core.Entities; using Bit.Core.Enums; @@ -58,6 +59,28 @@ public class PremiumUserBillingService( await userRepository.ReplaceAsync(user); } + public async Task UpdatePaymentMethod( + User user, + TokenizedPaymentSource tokenizedPaymentSource, + TaxInformation taxInformation) + { + if (string.IsNullOrEmpty(user.GatewayCustomerId)) + { + var customer = await CreateCustomerAsync(user, + new CustomerSetup { TokenizedPaymentSource = tokenizedPaymentSource, TaxInformation = taxInformation }); + + user.Gateway = GatewayType.Stripe; + user.GatewayCustomerId = customer.Id; + + await userRepository.ReplaceAsync(user); + } + else + { + await subscriberService.UpdatePaymentSource(user, tokenizedPaymentSource); + await subscriberService.UpdateTaxInformation(user, taxInformation); + } + } + private async Task CreateCustomerAsync( User user, CustomerSetup customerSetup) diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 157bfd3a6e..11d4042def 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -9,6 +9,7 @@ using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Services; using Bit.Core.Context; @@ -1044,11 +1045,11 @@ public class UserService : UserManager, IUserService, IDisposable throw new BadRequestException("Invalid token."); } - var updated = await _paymentService.UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken, taxInfo: taxInfo); - if (updated) - { - await SaveUserAsync(user); - } + var tokenizedPaymentSource = new TokenizedPaymentSource(paymentMethodType, paymentToken); + var taxInformation = TaxInformation.From(taxInfo); + + await _premiumUserBillingService.UpdatePaymentMethod(user, tokenizedPaymentSource, taxInformation); + await SaveUserAsync(user); } public async Task CancelPremiumAsync(User user, bool? endOfPeriod = null) From 5efd68cf51ab7437a6e92b4279c3dc8f73ac21db Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:07:02 -0600 Subject: [PATCH 076/148] [PM-17562] Initial POC of Distributed Events (#5323) * Initial POC of Distributed Events * Apply suggestions from code review Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * Clean up files to support accepted changes. Address PR Feedback * Removed unneeded using to fix lint warning * Moved config into a common EventLogging top-level item. Fixed issues from PR review * Optimized per suggestion from justinbaur Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * Updated to add IAsyncDisposable as suggested in PR review * Updated with suggestion to use KeyedSingleton for the IEventWriteService * Changed key case to lowercase --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- dev/.env.example | 6 +- dev/docker-compose.yml | 15 +++ .../RabbitMqEventHttpPostListener.cs | 35 +++++++ .../RabbitMqEventListenerBase.cs | 93 +++++++++++++++++++ .../RabbitMqEventRepositoryListener.cs | 29 ++++++ .../RabbitMqEventWriteService.cs | 65 +++++++++++++ src/Core/Core.csproj | 5 +- src/Core/Settings/GlobalSettings.cs | 39 ++++++++ src/Events/Startup.cs | 16 ++++ .../Utilities/ServiceCollectionExtensions.cs | 12 ++- 10 files changed, 311 insertions(+), 4 deletions(-) create mode 100644 src/Core/AdminConsole/Services/Implementations/RabbitMqEventHttpPostListener.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerBase.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/RabbitMqEventRepositoryListener.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs diff --git a/dev/.env.example b/dev/.env.example index d0ebf50efb..f0aed83a59 100644 --- a/dev/.env.example +++ b/dev/.env.example @@ -20,4 +20,8 @@ IDP_SP_ACS_URL=http://localhost:51822/saml2/yourOrgIdHere/Acs # Optional reverse proxy configuration # Should match server listen ports in reverse-proxy.conf API_PROXY_PORT=4100 -IDENTITY_PROXY_PORT=33756 \ No newline at end of file +IDENTITY_PROXY_PORT=33756 + +# Optional RabbitMQ configuration +RABBITMQ_DEFAULT_USER=bitwarden +RABBITMQ_DEFAULT_PASS=SET_A_PASSWORD_HERE_123 diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index c02d3c872b..d23eaefbb0 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -84,6 +84,20 @@ services: profiles: - idp + rabbitmq: + image: rabbitmq:management + container_name: rabbitmq + ports: + - "5672:5672" + - "15672:15672" + environment: + RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} + volumes: + - rabbitmq_data:/var/lib/rabbitmq_data + profiles: + - rabbitmq + reverse-proxy: image: nginx:alpine container_name: reverse-proxy @@ -99,3 +113,4 @@ volumes: mssql_dev_data: postgres_dev_data: mysql_dev_data: + rabbitmq_data: diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventHttpPostListener.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventHttpPostListener.cs new file mode 100644 index 0000000000..5a875f9278 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventHttpPostListener.cs @@ -0,0 +1,35 @@ +using System.Net.Http.Json; +using Bit.Core.Models.Data; +using Bit.Core.Settings; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Services; + +public class RabbitMqEventHttpPostListener : RabbitMqEventListenerBase +{ + private readonly HttpClient _httpClient; + private readonly string _httpPostUrl; + private readonly string _queueName; + + protected override string QueueName => _queueName; + + public const string HttpClientName = "EventHttpPostListenerHttpClient"; + + public RabbitMqEventHttpPostListener( + IHttpClientFactory httpClientFactory, + ILogger logger, + GlobalSettings globalSettings) + : base(logger, globalSettings) + { + _httpClient = httpClientFactory.CreateClient(HttpClientName); + _httpPostUrl = globalSettings.EventLogging.RabbitMq.HttpPostUrl; + _queueName = globalSettings.EventLogging.RabbitMq.HttpPostQueueName; + } + + protected override async Task HandleMessageAsync(EventMessage eventMessage) + { + var content = JsonContent.Create(eventMessage); + var response = await _httpClient.PostAsync(_httpPostUrl, content); + response.EnsureSuccessStatusCode(); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerBase.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerBase.cs new file mode 100644 index 0000000000..48a549d261 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerBase.cs @@ -0,0 +1,93 @@ +using System.Text.Json; +using Bit.Core.Models.Data; +using Bit.Core.Settings; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace Bit.Core.Services; + +public abstract class RabbitMqEventListenerBase : BackgroundService +{ + private IChannel _channel; + private IConnection _connection; + private readonly string _exchangeName; + private readonly ConnectionFactory _factory; + private readonly ILogger _logger; + + protected abstract string QueueName { get; } + + protected RabbitMqEventListenerBase( + ILogger logger, + GlobalSettings globalSettings) + { + _factory = new ConnectionFactory + { + HostName = globalSettings.EventLogging.RabbitMq.HostName, + UserName = globalSettings.EventLogging.RabbitMq.Username, + Password = globalSettings.EventLogging.RabbitMq.Password + }; + _exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName; + _logger = logger; + } + + public override async Task StartAsync(CancellationToken cancellationToken) + { + _connection = await _factory.CreateConnectionAsync(cancellationToken); + _channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken); + + await _channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true); + await _channel.QueueDeclareAsync(queue: QueueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null, + cancellationToken: cancellationToken); + await _channel.QueueBindAsync(queue: QueueName, + exchange: _exchangeName, + routingKey: string.Empty, + cancellationToken: cancellationToken); + await base.StartAsync(cancellationToken); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var consumer = new AsyncEventingBasicConsumer(_channel); + consumer.ReceivedAsync += async (_, eventArgs) => + { + try + { + var eventMessage = JsonSerializer.Deserialize(eventArgs.Body.Span); + await HandleMessageAsync(eventMessage); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while processing the message"); + } + }; + + await _channel.BasicConsumeAsync(QueueName, autoAck: true, consumer: consumer, cancellationToken: stoppingToken); + + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(1_000, stoppingToken); + } + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + await _channel.CloseAsync(); + await _connection.CloseAsync(); + await base.StopAsync(cancellationToken); + } + + public override void Dispose() + { + _channel.Dispose(); + _connection.Dispose(); + base.Dispose(); + } + + protected abstract Task HandleMessageAsync(EventMessage eventMessage); +} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventRepositoryListener.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventRepositoryListener.cs new file mode 100644 index 0000000000..25d85bddeb --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventRepositoryListener.cs @@ -0,0 +1,29 @@ +using Bit.Core.Models.Data; +using Bit.Core.Settings; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Services; + +public class RabbitMqEventRepositoryListener : RabbitMqEventListenerBase +{ + private readonly IEventWriteService _eventWriteService; + private readonly string _queueName; + + protected override string QueueName => _queueName; + + public RabbitMqEventRepositoryListener( + [FromKeyedServices("persistent")] IEventWriteService eventWriteService, + ILogger logger, + GlobalSettings globalSettings) + : base(logger, globalSettings) + { + _eventWriteService = eventWriteService; + _queueName = globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName; + } + + protected override Task HandleMessageAsync(EventMessage eventMessage) + { + return _eventWriteService.CreateAsync(eventMessage); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs new file mode 100644 index 0000000000..d89cf890ac --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs @@ -0,0 +1,65 @@ +using System.Text.Json; +using Bit.Core.Models.Data; +using Bit.Core.Settings; +using RabbitMQ.Client; + +namespace Bit.Core.Services; +public class RabbitMqEventWriteService : IEventWriteService, IAsyncDisposable +{ + private readonly ConnectionFactory _factory; + private readonly Lazy> _lazyConnection; + private readonly string _exchangeName; + + public RabbitMqEventWriteService(GlobalSettings globalSettings) + { + _factory = new ConnectionFactory + { + HostName = globalSettings.EventLogging.RabbitMq.HostName, + UserName = globalSettings.EventLogging.RabbitMq.Username, + Password = globalSettings.EventLogging.RabbitMq.Password + }; + _exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName; + + _lazyConnection = new Lazy>(CreateConnectionAsync); + } + + public async Task CreateAsync(IEvent e) + { + var connection = await _lazyConnection.Value; + using var channel = await connection.CreateChannelAsync(); + + await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true); + + var body = JsonSerializer.SerializeToUtf8Bytes(e); + + await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: string.Empty, body: body); + } + + public async Task CreateManyAsync(IEnumerable events) + { + var connection = await _lazyConnection.Value; + using var channel = await connection.CreateChannelAsync(); + await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true); + + foreach (var e in events) + { + var body = JsonSerializer.SerializeToUtf8Bytes(e); + + await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: string.Empty, body: body); + } + } + + public async ValueTask DisposeAsync() + { + if (_lazyConnection.IsValueCreated) + { + var connection = await _lazyConnection.Value; + await connection.DisposeAsync(); + } + } + + private async Task CreateConnectionAsync() + { + return await _factory.CreateConnectionAsync(); + } +} diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 7a5f7e2543..210a33f3f7 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -40,7 +40,7 @@ - + @@ -70,12 +70,13 @@ + - + diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 97d66aed53..718293891b 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -53,6 +53,7 @@ public class GlobalSettings : IGlobalSettings public virtual SqlSettings PostgreSql { get; set; } = new SqlSettings(); public virtual SqlSettings MySql { get; set; } = new SqlSettings(); public virtual SqlSettings Sqlite { get; set; } = new SqlSettings() { ConnectionString = "Data Source=:memory:" }; + public virtual EventLoggingSettings EventLogging { get; set; } = new EventLoggingSettings(); public virtual MailSettings Mail { get; set; } = new MailSettings(); public virtual IConnectionStringSettings Storage { get; set; } = new ConnectionStringSettings(); public virtual ConnectionStringSettings Events { get; set; } = new ConnectionStringSettings(); @@ -256,6 +257,44 @@ public class GlobalSettings : IGlobalSettings } } + public class EventLoggingSettings + { + public RabbitMqSettings RabbitMq { get; set; } + + public class RabbitMqSettings + { + private string _hostName; + private string _username; + private string _password; + private string _exchangeName; + + public virtual string EventRepositoryQueueName { get; set; } = "events-write-queue"; + public virtual string HttpPostQueueName { get; set; } = "events-httpPost-queue"; + public virtual string HttpPostUrl { get; set; } + + public string HostName + { + get => _hostName; + set => _hostName = value.Trim('"'); + } + public string Username + { + get => _username; + set => _username = value.Trim('"'); + } + public string Password + { + get => _password; + set => _password = value.Trim('"'); + } + public string ExchangeName + { + get => _exchangeName; + set => _exchangeName = value.Trim('"'); + } + } + } + public class ConnectionStringSettings : IConnectionStringSettings { private string _connectionString; diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index bac39c68dd..03e99f14e8 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -82,6 +82,22 @@ public class Startup { services.AddHostedService(); } + + // Optional RabbitMQ Listeners + if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName)) + { + services.AddKeyedSingleton("persistent"); + services.AddHostedService(); + + if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HttpPostUrl)) + { + services.AddHttpClient(RabbitMqEventHttpPostListener.HttpClientName); + services.AddHostedService(); + } + } } public void Configure( diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index e1369d5366..622b3d7f39 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -325,7 +325,17 @@ public static class ServiceCollectionExtensions } else if (globalSettings.SelfHosted) { - services.AddSingleton(); + if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName)) + { + services.AddSingleton(); + } + else + { + services.AddSingleton(); + } } else { From ab0cab2072d7c925752ee7aa63125cbe2f10ed22 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 30 Jan 2025 13:59:58 -0500 Subject: [PATCH 077/148] Fix Events Startup (#5352) --- src/Core/Settings/GlobalSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 718293891b..d039102eb9 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -259,7 +259,7 @@ public class GlobalSettings : IGlobalSettings public class EventLoggingSettings { - public RabbitMqSettings RabbitMq { get; set; } + public RabbitMqSettings RabbitMq { get; set; } = new RabbitMqSettings(); public class RabbitMqSettings { From 148a6311783b2db87eab6781327570d26eec6e5e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:59:39 +0100 Subject: [PATCH 078/148] [deps]: Update github/codeql-action action to v3.28.8 (#5292) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/scan.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7d64612aba..3b96eeb468 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -314,7 +314,7 @@ jobs: output-format: sarif - name: Upload Grype results to GitHub - uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 + uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 with: sarif_file: ${{ steps.container-scan.outputs.sarif }} diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 156ebee165..ec2eb7789a 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -46,7 +46,7 @@ jobs: --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 + uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 with: sarif_file: cx_result.sarif From d239170c1ca3996703c59ea3a46cf6315b925a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:01:26 +0000 Subject: [PATCH 079/148] [PM-17697] Save Organization Name changes in Bitwarden Portal (#5337) * Add Org_Name_Edit permission to the Permissions enum * Add Org_Name_Edit permission to RolePermissionMapping * Implement Org_Name_Edit permission check in UpdateOrganization method * Add Org_Name_Edit permission check to Organization form input --- .../AdminConsole/Controllers/OrganizationsController.cs | 5 +++++ src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml | 3 ++- src/Admin/Enums/Permissions.cs | 1 + src/Admin/Utilities/RolePermissionMapping.cs | 5 +++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index 3fdef169b4..60a5a39612 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -421,6 +421,11 @@ public class OrganizationsController : Controller private void UpdateOrganization(Organization organization, OrganizationEditModel model) { + if (_accessControlService.UserHasPermission(Permission.Org_Name_Edit)) + { + organization.Name = WebUtility.HtmlEncode(model.Name); + } + if (_accessControlService.UserHasPermission(Permission.Org_CheckEnabledBox)) { organization.Enabled = model.Enabled; diff --git a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml index cdc7608675..aeff65c900 100644 --- a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml +++ b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml @@ -12,6 +12,7 @@ var canViewBilling = AccessControlService.UserHasPermission(Permission.Org_Billing_View); var canViewPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_View); var canViewLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_View); + var canEditName = AccessControlService.UserHasPermission(Permission.Org_Name_Edit); var canCheckEnabled = AccessControlService.UserHasPermission(Permission.Org_CheckEnabledBox); var canEditPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_Edit); var canEditLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_Edit); @@ -28,7 +29,7 @@
- +
diff --git a/src/Admin/Enums/Permissions.cs b/src/Admin/Enums/Permissions.cs index 20c500c061..4edcd742b4 100644 --- a/src/Admin/Enums/Permissions.cs +++ b/src/Admin/Enums/Permissions.cs @@ -22,6 +22,7 @@ public enum Permission Org_List_View, Org_OrgInformation_View, Org_GeneralDetails_View, + Org_Name_Edit, Org_CheckEnabledBox, Org_BusinessInformation_View, Org_InitiateTrial, diff --git a/src/Admin/Utilities/RolePermissionMapping.cs b/src/Admin/Utilities/RolePermissionMapping.cs index 4b5a4e3802..3b510781be 100644 --- a/src/Admin/Utilities/RolePermissionMapping.cs +++ b/src/Admin/Utilities/RolePermissionMapping.cs @@ -24,6 +24,7 @@ public static class RolePermissionMapping Permission.User_Billing_Edit, Permission.User_Billing_LaunchGateway, Permission.User_NewDeviceException_Edit, + Permission.Org_Name_Edit, Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, @@ -71,6 +72,7 @@ public static class RolePermissionMapping Permission.User_Billing_Edit, Permission.User_Billing_LaunchGateway, Permission.User_NewDeviceException_Edit, + Permission.Org_Name_Edit, Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, @@ -116,6 +118,7 @@ public static class RolePermissionMapping Permission.User_Billing_View, Permission.User_Billing_LaunchGateway, Permission.User_NewDeviceException_Edit, + Permission.Org_Name_Edit, Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, @@ -148,6 +151,7 @@ public static class RolePermissionMapping Permission.User_Billing_View, Permission.User_Billing_Edit, Permission.User_Billing_LaunchGateway, + Permission.Org_Name_Edit, Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, @@ -185,6 +189,7 @@ public static class RolePermissionMapping Permission.User_Premium_View, Permission.User_Licensing_View, Permission.User_Licensing_Edit, + Permission.Org_Name_Edit, Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, From e43a8011f10d94d9ca3271fe2a21e703ce6023d1 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:46:09 -0500 Subject: [PATCH 080/148] [PM-17709] Send New Device Login email for all new devices (#5340) * Send New Device Login email regardless of New Device Verification * Adjusted tests * Linting * Clarified test names. --- .../RequestValidators/DeviceValidator.cs | 34 ++++++++++--------- .../IdentityServer/DeviceValidatorTests.cs | 12 ++----- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs index 1b148c5974..fee10e10ff 100644 --- a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs @@ -85,28 +85,17 @@ public class DeviceValidator( } } - // At this point we have established either new device verification is not required or the NewDeviceOtp is valid + // At this point we have established either new device verification is not required or the NewDeviceOtp is valid, + // so we save the device to the database and proceed with authentication requestDevice.UserId = context.User.Id; 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)) + if (!_globalSettings.DisableEmailNewDevice) { - // 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); - } - } + await SendNewDeviceLoginEmail(context.User, requestDevice); } + return true; } @@ -174,6 +163,19 @@ public class DeviceValidator( return DeviceValidationResultType.NewDeviceVerificationRequired; } + private async Task SendNewDeviceLoginEmail(User user, Device requestDevice) + { + // Ensure that the user doesn't receive a "new device" email on the first login + var now = DateTime.UtcNow; + if (now - user.CreationDate > TimeSpan.FromMinutes(10)) + { + var deviceType = requestDevice.Type.GetType().GetMember(requestDevice.Type.ToString()) + .FirstOrDefault()?.GetCustomAttribute()?.GetName(); + await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now, + _currentContext.IpAddress); + } + } + public async Task GetKnownDeviceAsync(User user, Device device) { if (user == null || device == null) diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index fa3a117c55..6e6406f16b 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -227,7 +227,7 @@ public class DeviceValidatorTests } [Theory, BitAutoData] - public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_SendsEmail_ReturnsTrue( + public async void ValidateRequestDeviceAsync_ExistingUserNewDeviceLogin_SendNewDeviceLoginEmail_ReturnsTrue( CustomValidatorRequestContext context, [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) { @@ -237,8 +237,6 @@ public class DeviceValidatorTests _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); @@ -253,7 +251,7 @@ public class DeviceValidatorTests } [Theory, BitAutoData] - public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_NewUser_DoesNotSendEmail_ReturnsTrue( + public async void ValidateRequestDeviceAsync_NewUserNewDeviceLogin_DoesNotSendNewDeviceLoginEmail_ReturnsTrue( CustomValidatorRequestContext context, [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) { @@ -263,8 +261,6 @@ public class DeviceValidatorTests _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); @@ -279,7 +275,7 @@ public class DeviceValidatorTests } [Theory, BitAutoData] - public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_DisableEmailTrue_DoesNotSendEmail_ReturnsTrue( + public async void ValidateRequestDeviceAsynce_DisableNewDeviceLoginEmailTrue_DoesNotSendNewDeviceEmail_ReturnsTrue( CustomValidatorRequestContext context, [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) { @@ -289,8 +285,6 @@ public class DeviceValidatorTests _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); From bd394eabe9c887e43893a961e191e6c0e11e1d08 Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Fri, 31 Jan 2025 10:50:14 -0500 Subject: [PATCH 081/148] [pm-16528] Fix entity framework query (#5333) --- .../OrganizationDomainRepository.cs | 28 ++--- .../OrganizationDomainRepositoryTests.cs | 118 ++++++++++++++++++ 2 files changed, 127 insertions(+), 19 deletions(-) diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs index e339c13351..50d791b81b 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs @@ -46,27 +46,17 @@ public class OrganizationDomainRepository : Repository x.VerifiedDate == null - && x.JobRunCount != 3 - && x.NextRunDate.Year == date.Year - && x.NextRunDate.Month == date.Month - && x.NextRunDate.Day == date.Day - && x.NextRunDate.Hour == date.Hour) - .AsNoTracking() + var start36HoursWindow = date.AddHours(-36); + var end36HoursWindow = date; + + var pastDomains = await dbContext.OrganizationDomains + .Where(x => x.NextRunDate >= start36HoursWindow + && x.NextRunDate <= end36HoursWindow + && x.VerifiedDate == null + && x.JobRunCount != 3) .ToListAsync(); - //Get records that have ignored/failed by the background service - var pastDomains = dbContext.OrganizationDomains - .AsEnumerable() - .Where(x => (date - x.NextRunDate).TotalHours > 36 - && x.VerifiedDate == null - && x.JobRunCount != 3) - .ToList(); - - var results = domains.Union(pastDomains); - - return Mapper.Map>(results); + return Mapper.Map>(pastDomains); } public async Task GetOrganizationDomainSsoDetailsAsync(string email) diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs index 8e0b502a47..ad92f40efc 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs @@ -188,4 +188,122 @@ public class OrganizationDomainRepositoryTests var expectedDomain2 = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain2.DomainName); Assert.Null(expectedDomain2); } + + [DatabaseTheory, DatabaseData] + public async Task GetManyByNextRunDateAsync_ShouldReturnUnverifiedDomains( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + // Arrange + var id = Guid.NewGuid(); + + var organization1 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = $"test+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization1.Id, + DomainName = $"domain2+{id}@example.com", + Txt = "btw+12345" + }; + + var within36HoursWindow = 1; + organizationDomain.SetNextRunDate(within36HoursWindow); + + await organizationDomainRepository.CreateAsync(organizationDomain); + + var date = organizationDomain.NextRunDate; + + // Act + var domains = await organizationDomainRepository.GetManyByNextRunDateAsync(date); + + // Assert + var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName); + Assert.NotNull(expectedDomain); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyByNextRunDateAsync_ShouldNotReturnUnverifiedDomains_WhenNextRunDateIsOutside36hoursWindow( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + // Arrange + var id = Guid.NewGuid(); + + var organization1 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = $"test+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization1.Id, + DomainName = $"domain2+{id}@example.com", + Txt = "btw+12345" + }; + + var outside36HoursWindow = 20; + organizationDomain.SetNextRunDate(outside36HoursWindow); + + await organizationDomainRepository.CreateAsync(organizationDomain); + + var date = DateTimeOffset.UtcNow.Date.AddDays(1); + + // Act + var domains = await organizationDomainRepository.GetManyByNextRunDateAsync(date); + + // Assert + var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName); + Assert.Null(expectedDomain); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyByNextRunDateAsync_ShouldNotReturnVerifiedDomains( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + // Arrange + var id = Guid.NewGuid(); + + var organization1 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = $"test+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization1.Id, + DomainName = $"domain2+{id}@example.com", + Txt = "btw+12345" + }; + + var within36HoursWindow = 1; + organizationDomain.SetNextRunDate(within36HoursWindow); + organizationDomain.SetVerifiedDate(); + + await organizationDomainRepository.CreateAsync(organizationDomain); + + var date = DateTimeOffset.UtcNow.Date.AddDays(1); + + // Act + var domains = await organizationDomainRepository.GetManyByNextRunDateAsync(date); + + // Assert + var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName); + Assert.Null(expectedDomain); + } } From 408ddd938893448cfce270ef0754168d4d65e037 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 31 Jan 2025 11:08:07 -0500 Subject: [PATCH 082/148] Scaffold Events Integration Tests (#5355) * Scaffold Events Integration Tests * Format --- bitwarden-server.sln | 7 ++ .../Controllers/CollectControllerTests.cs | 29 ++++++++ .../Events.IntegrationTest.csproj | 29 ++++++++ .../EventsApplicationFactory.cs | 57 +++++++++++++++ test/Events.IntegrationTest/GlobalUsings.cs | 1 + .../Factories/WebApplicationFactoryBase.cs | 72 ++++++++++--------- 6 files changed, 163 insertions(+), 32 deletions(-) create mode 100644 test/Events.IntegrationTest/Controllers/CollectControllerTests.cs create mode 100644 test/Events.IntegrationTest/Events.IntegrationTest.csproj create mode 100644 test/Events.IntegrationTest/EventsApplicationFactory.cs create mode 100644 test/Events.IntegrationTest/GlobalUsings.cs diff --git a/bitwarden-server.sln b/bitwarden-server.sln index 75e7d7fade..e9aff53f8e 100644 --- a/bitwarden-server.sln +++ b/bitwarden-server.sln @@ -125,6 +125,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notifications.Test", "test\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.Dapper.Test", "test\Infrastructure.Dapper.Test\Infrastructure.Dapper.Test.csproj", "{4A725DB3-BE4F-4C23-9087-82D0610D67AF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "test\Events.IntegrationTest\Events.IntegrationTest.csproj", "{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -313,6 +315,10 @@ Global {4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Debug|Any CPU.Build.0 = Debug|Any CPU {4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.ActiveCfg = Release|Any CPU {4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.Build.0 = Release|Any CPU + {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -363,6 +369,7 @@ Global {81673EFB-7134-4B4B-A32F-1EA05F0EF3CE} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {90D85D8F-5577-4570-A96E-5A2E185F0F6F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} + {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} diff --git a/test/Events.IntegrationTest/Controllers/CollectControllerTests.cs b/test/Events.IntegrationTest/Controllers/CollectControllerTests.cs new file mode 100644 index 0000000000..7f86758144 --- /dev/null +++ b/test/Events.IntegrationTest/Controllers/CollectControllerTests.cs @@ -0,0 +1,29 @@ +using System.Net.Http.Json; +using Bit.Core.Enums; +using Bit.Events.Models; + +namespace Bit.Events.IntegrationTest.Controllers; + +public class CollectControllerTests +{ + // This is a very simple test, and should be updated to assert more things, but for now + // it ensures that the events startup doesn't throw any errors with fairly basic configuration. + [Fact] + public async Task Post_Works() + { + var eventsApplicationFactory = new EventsApplicationFactory(); + var (accessToken, _) = await eventsApplicationFactory.LoginWithNewAccount(); + var client = eventsApplicationFactory.CreateAuthedClient(accessToken); + + var response = await client.PostAsJsonAsync>("collect", + [ + new EventModel + { + Type = EventType.User_ClientExportedVault, + Date = DateTime.UtcNow, + }, + ]); + + response.EnsureSuccessStatusCode(); + } +} diff --git a/test/Events.IntegrationTest/Events.IntegrationTest.csproj b/test/Events.IntegrationTest/Events.IntegrationTest.csproj new file mode 100644 index 0000000000..0b51185298 --- /dev/null +++ b/test/Events.IntegrationTest/Events.IntegrationTest.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/test/Events.IntegrationTest/EventsApplicationFactory.cs b/test/Events.IntegrationTest/EventsApplicationFactory.cs new file mode 100644 index 0000000000..3faf5e81bf --- /dev/null +++ b/test/Events.IntegrationTest/EventsApplicationFactory.cs @@ -0,0 +1,57 @@ +using Bit.Identity.Models.Request.Accounts; +using Bit.IntegrationTestCommon.Factories; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Events.IntegrationTest; + +public class EventsApplicationFactory : WebApplicationFactoryBase +{ + private readonly IdentityApplicationFactory _identityApplicationFactory; + private const string _connectionString = "DataSource=:memory:"; + + public EventsApplicationFactory() + { + SqliteConnection = new SqliteConnection(_connectionString); + SqliteConnection.Open(); + + _identityApplicationFactory = new IdentityApplicationFactory(); + _identityApplicationFactory.SqliteConnection = SqliteConnection; + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + base.ConfigureWebHost(builder); + + builder.ConfigureTestServices(services => + { + services.Configure(JwtBearerDefaults.AuthenticationScheme, options => + { + options.BackchannelHttpHandler = _identityApplicationFactory.Server.CreateHandler(); + }); + }); + } + + /// + /// Helper for registering and logging in to a new account + /// + public async Task<(string Token, string RefreshToken)> LoginWithNewAccount(string email = "integration-test@bitwarden.com", string masterPasswordHash = "master_password_hash") + { + await _identityApplicationFactory.RegisterAsync(new RegisterRequestModel + { + Email = email, + MasterPasswordHash = masterPasswordHash, + }); + + return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + SqliteConnection!.Dispose(); + } +} diff --git a/test/Events.IntegrationTest/GlobalUsings.cs b/test/Events.IntegrationTest/GlobalUsings.cs new file mode 100644 index 0000000000..9df1d42179 --- /dev/null +++ b/test/Events.IntegrationTest/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs index d01e92ad4c..7c7f790cdc 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs @@ -14,6 +14,7 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; @@ -188,44 +189,27 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory // QUESTION: The normal licensing service should run fine on developer machines but not in CI // should we have a fork here to leave the normal service for developers? // TODO: Eventually add the license file to CI - var licensingService = services.First(sd => sd.ServiceType == typeof(ILicensingService)); - services.Remove(licensingService); - services.AddSingleton(); + Replace(services); // FUTURE CONSIDERATION: Add way to run this self hosted/cloud, for now it is cloud only - var pushRegistrationService = services.First(sd => sd.ServiceType == typeof(IPushRegistrationService)); - services.Remove(pushRegistrationService); - services.AddSingleton(); + Replace(services); // Even though we are cloud we currently set this up as cloud, we can use the EF/selfhosted service // instead of using Noop for this service // TODO: Install and use azurite in CI pipeline - var eventWriteService = services.First(sd => sd.ServiceType == typeof(IEventWriteService)); - services.Remove(eventWriteService); - services.AddSingleton(); + Replace(services); - var eventRepositoryService = services.First(sd => sd.ServiceType == typeof(IEventRepository)); - services.Remove(eventRepositoryService); - services.AddSingleton(); + Replace(services); - var mailDeliveryService = services.First(sd => sd.ServiceType == typeof(IMailDeliveryService)); - services.Remove(mailDeliveryService); - services.AddSingleton(); + Replace(services); - var captchaValidationService = services.First(sd => sd.ServiceType == typeof(ICaptchaValidationService)); - services.Remove(captchaValidationService); - services.AddSingleton(); + Replace(services); // TODO: Install and use azurite in CI pipeline - var installationDeviceRepository = - services.First(sd => sd.ServiceType == typeof(IInstallationDeviceRepository)); - services.Remove(installationDeviceRepository); - services.AddSingleton(); + Replace(services); // TODO: Install and use azurite in CI pipeline - var referenceEventService = services.First(sd => sd.ServiceType == typeof(IReferenceEventService)); - services.Remove(referenceEventService); - services.AddSingleton(); + Replace(services); // Our Rate limiter works so well that it begins to fail tests unless we carve out // one whitelisted ip. We should still test the rate limiter though and they should change the Ip @@ -245,14 +229,9 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory services.AddSingleton(); // Noop StripePaymentService - this could be changed to integrate with our Stripe test account - var stripePaymentService = services.First(sd => sd.ServiceType == typeof(IPaymentService)); - services.Remove(stripePaymentService); - services.AddSingleton(Substitute.For()); + Replace(services, Substitute.For()); - var organizationBillingService = - services.First(sd => sd.ServiceType == typeof(IOrganizationBillingService)); - services.Remove(organizationBillingService); - services.AddSingleton(Substitute.For()); + Replace(services, Substitute.For()); }); foreach (var configureTestService in _configureTestServices) @@ -261,6 +240,35 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory } } + private static void Replace(IServiceCollection services) + where TService : class + where TNewImplementation : class, TService + { + services.RemoveAll(); + services.AddSingleton(); + } + + private static void Replace(IServiceCollection services, TService implementation) + where TService : class + { + services.RemoveAll(); + services.AddSingleton(implementation); + } + + public HttpClient CreateAuthedClient(string accessToken) + { + var handler = Server.CreateHandler((context) => + { + context.Request.Headers.Authorization = $"Bearer {accessToken}"; + }); + + return new HttpClient(handler) + { + BaseAddress = Server.BaseAddress, + Timeout = TimeSpan.FromSeconds(200), + }; + } + public DatabaseContext GetDatabaseContext() { var scope = Services.CreateScope(); From 669c253bc62639deffd08076843873030d66e223 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:18:10 -0600 Subject: [PATCH 083/148] chore: add limit item deletion feature flag constant, refs PM-17214 (#5356) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 6d70f0b3ce..5643ed7654 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -107,6 +107,7 @@ public static class FeatureFlagKeys public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; public const string IntegrationPage = "pm-14505-admin-console-integration-page"; public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications"; + public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission"; public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection"; From 1adc5358a871885483296e2a997a147ab17fd84d Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Mon, 3 Feb 2025 09:35:38 -0500 Subject: [PATCH 084/148] Create a single feature flag for the Authenticator sync (#5353) * Create a single feature flag for the Authenticator sync * Update feature flag key --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 5643ed7654..b196306409 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -171,6 +171,7 @@ public static class FeatureFlagKeys public const string SingleTapPasskeyCreation = "single-tap-passkey-creation"; public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication"; public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications"; + public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync"; public static List GetAllKeys() { From fe983aff7f43b076f9c1affeb654eaa9951d2fff Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Mon, 3 Feb 2025 12:35:46 -0500 Subject: [PATCH 085/148] [pm-17911] Refresh OrganizationView (#5360) --- .../2025-02-03_01_RefreshView_For_LimitItemDeletion.sql | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 util/Migrator/DbScripts/2025-02-03_01_RefreshView_For_LimitItemDeletion.sql diff --git a/util/Migrator/DbScripts/2025-02-03_01_RefreshView_For_LimitItemDeletion.sql b/util/Migrator/DbScripts/2025-02-03_01_RefreshView_For_LimitItemDeletion.sql new file mode 100644 index 0000000000..98893bb030 --- /dev/null +++ b/util/Migrator/DbScripts/2025-02-03_01_RefreshView_For_LimitItemDeletion.sql @@ -0,0 +1,7 @@ +-- Refresh Views + +IF OBJECT_ID('[dbo].[OrganizationView]') IS NOT NULL + BEGIN + EXECUTE sp_refreshview N'[dbo].[OrganizationView]'; + END +GO From 060e9e60bff549b65cef12586c4fab0ec598a7fe Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Mon, 3 Feb 2025 14:55:57 -0500 Subject: [PATCH 086/148] [pm-337] Remove the continuation token from the ListResponseModel. (#5192) --- .../Public/Controllers/EventsController.cs | 4 ++-- src/Api/Models/Public/Response/ListResponseModel.cs | 7 +------ .../Models/Public/Response/PagedListResponseModel.cs | 10 ++++++++++ 3 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 src/Api/Models/Public/Response/PagedListResponseModel.cs diff --git a/src/Api/AdminConsole/Public/Controllers/EventsController.cs b/src/Api/AdminConsole/Public/Controllers/EventsController.cs index d2e198de17..992b7453aa 100644 --- a/src/Api/AdminConsole/Public/Controllers/EventsController.cs +++ b/src/Api/AdminConsole/Public/Controllers/EventsController.cs @@ -36,7 +36,7 @@ public class EventsController : Controller /// If no filters are provided, it will return the last 30 days of event for the organization. /// [HttpGet] - [ProducesResponseType(typeof(ListResponseModel), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(PagedListResponseModel), (int)HttpStatusCode.OK)] public async Task List([FromQuery] EventFilterRequestModel request) { var dateRange = request.ToDateRange(); @@ -65,7 +65,7 @@ public class EventsController : Controller } var eventResponses = result.Data.Select(e => new EventResponseModel(e)); - var response = new ListResponseModel(eventResponses, result.ContinuationToken); + var response = new PagedListResponseModel(eventResponses, result.ContinuationToken); return new JsonResult(response); } } diff --git a/src/Api/Models/Public/Response/ListResponseModel.cs b/src/Api/Models/Public/Response/ListResponseModel.cs index 0865be3e8e..a55d6f62bb 100644 --- a/src/Api/Models/Public/Response/ListResponseModel.cs +++ b/src/Api/Models/Public/Response/ListResponseModel.cs @@ -4,10 +4,9 @@ namespace Bit.Api.Models.Public.Response; public class ListResponseModel : IResponseModel where T : IResponseModel { - public ListResponseModel(IEnumerable data, string continuationToken = null) + public ListResponseModel(IEnumerable data) { Data = data; - ContinuationToken = continuationToken; } /// @@ -21,8 +20,4 @@ public class ListResponseModel : IResponseModel where T : IResponseModel /// [Required] public IEnumerable Data { get; set; } - /// - /// A cursor for use in pagination. - /// - public string ContinuationToken { get; set; } } diff --git a/src/Api/Models/Public/Response/PagedListResponseModel.cs b/src/Api/Models/Public/Response/PagedListResponseModel.cs new file mode 100644 index 0000000000..b0f25cb4f8 --- /dev/null +++ b/src/Api/Models/Public/Response/PagedListResponseModel.cs @@ -0,0 +1,10 @@ +namespace Bit.Api.Models.Public.Response; + +public class PagedListResponseModel(IEnumerable data, string continuationToken) : ListResponseModel(data) + where T : IResponseModel +{ + /// + /// A cursor for use in pagination. + /// + public string ContinuationToken { get; set; } = continuationToken; +} From 90f308db34a6ce79967f3a93173bd32137125660 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:09:09 +0100 Subject: [PATCH 087/148] [deps] Tools: Update aws-sdk-net monorepo (#5278) 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 210a33f3f7..7b319e56c9 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,8 +21,8 @@ - - + + From f1b9bd9a099d6d542eb367597c7ef99e9842c233 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 4 Feb 2025 09:02:18 -0500 Subject: [PATCH 088/148] [PM-15179] Implement endpoints to add existing organization to CB provider (#5310) * Implement endpoints to add existing organization to provider * Run dotnet format * Support MOE * Run dotnet format * Move ProviderClientsController under AC ownership * Move ProviderClientsControllerTests under AC ownership * Jared's feedback --- .../Billing/ProviderBillingService.cs | 179 ++++++++++++++++++ .../Controllers/ProviderClientsController.cs | 68 ++++++- .../AddExistingOrganizationRequestBody.cs | 12 ++ .../Repositories/IOrganizationRepository.cs | 2 + src/Core/Billing/Constants/PlanConstants.cs | 30 +++ src/Core/Billing/Constants/StripeConstants.cs | 10 + .../Billing/Models/AddableOrganization.cs | 8 + .../Services/IProviderBillingService.cs | 10 + src/Core/Constants.cs | 1 + .../Repositories/OrganizationRepository.cs | 16 ++ .../Repositories/OrganizationRepository.cs | 38 ++++ ...nization_ReadAddableToProviderByUserId.sql | 23 +++ .../ProviderClientsControllerTests.cs | 5 +- ...nization_ReadAddableToProviderByUserId.sql | 31 +++ 14 files changed, 427 insertions(+), 6 deletions(-) rename src/Api/{Billing => AdminConsole}/Controllers/ProviderClientsController.cs (67%) create mode 100644 src/Api/Billing/Models/Requests/AddExistingOrganizationRequestBody.cs create mode 100644 src/Core/Billing/Constants/PlanConstants.cs create mode 100644 src/Core/Billing/Models/AddableOrganization.cs create mode 100644 src/Sql/dbo/Stored Procedures/Organization_ReadAddableToProviderByUserId.sql rename test/Api.Test/{Billing => AdminConsole}/Controllers/ProviderClientsControllerTests.cs (98%) create mode 100644 util/Migrator/DbScripts/2025-01-28_00_Add_Organization_ReadAddableToProviderByUserId.sql diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index 2b834947af..abba8aff90 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -1,12 +1,15 @@ using System.Globalization; using Bit.Commercial.Core.Billing.Models; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Contracts; @@ -24,6 +27,7 @@ using Stripe; namespace Bit.Commercial.Core.Billing; public class ProviderBillingService( + IEventService eventService, IGlobalSettings globalSettings, ILogger logger, IOrganizationRepository organizationRepository, @@ -31,10 +35,93 @@ public class ProviderBillingService( IProviderInvoiceItemRepository providerInvoiceItemRepository, IProviderOrganizationRepository providerOrganizationRepository, IProviderPlanRepository providerPlanRepository, + IProviderUserRepository providerUserRepository, IStripeAdapter stripeAdapter, ISubscriberService subscriberService, ITaxService taxService) : IProviderBillingService { + [RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)] + public async Task AddExistingOrganization( + Provider provider, + Organization organization, + string key) + { + await stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, + new SubscriptionUpdateOptions + { + CancelAtPeriodEnd = false + }); + + var subscription = + await stripeAdapter.SubscriptionCancelAsync(organization.GatewaySubscriptionId, + new SubscriptionCancelOptions + { + CancellationDetails = new SubscriptionCancellationDetailsOptions + { + Comment = $"Organization was added to Provider with ID {provider.Id}" + }, + InvoiceNow = true, + Prorate = true, + Expand = ["latest_invoice", "test_clock"] + }); + + var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow; + + var wasTrialing = subscription.TrialEnd.HasValue && subscription.TrialEnd.Value > now; + + if (!wasTrialing && subscription.LatestInvoice.Status == StripeConstants.InvoiceStatus.Draft) + { + await stripeAdapter.InvoiceFinalizeInvoiceAsync(subscription.LatestInvoiceId, + new InvoiceFinalizeOptions { AutoAdvance = true }); + } + + var managedPlanType = await GetManagedPlanTypeAsync(provider, organization); + + // TODO: Replace with PricingClient + var plan = StaticStore.GetPlan(managedPlanType); + organization.Plan = plan.Name; + organization.PlanType = plan.Type; + organization.MaxCollections = plan.PasswordManager.MaxCollections; + organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb; + organization.UsePolicies = plan.HasPolicies; + organization.UseSso = plan.HasSso; + organization.UseGroups = plan.HasGroups; + organization.UseEvents = plan.HasEvents; + organization.UseDirectory = plan.HasDirectory; + organization.UseTotp = plan.HasTotp; + organization.Use2fa = plan.Has2fa; + organization.UseApi = plan.HasApi; + organization.UseResetPassword = plan.HasResetPassword; + organization.SelfHost = plan.HasSelfHost; + organization.UsersGetPremium = plan.UsersGetPremium; + organization.UseCustomPermissions = plan.HasCustomPermissions; + organization.UseScim = plan.HasScim; + organization.UseKeyConnector = plan.HasKeyConnector; + organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb; + organization.BillingEmail = provider.BillingEmail!; + organization.GatewaySubscriptionId = null; + organization.ExpirationDate = null; + organization.MaxAutoscaleSeats = null; + organization.Status = OrganizationStatusType.Managed; + + var providerOrganization = new ProviderOrganization + { + ProviderId = provider.Id, + OrganizationId = organization.Id, + Key = key + }; + + await Task.WhenAll( + organizationRepository.ReplaceAsync(organization), + providerOrganizationRepository.CreateAsync(providerOrganization), + ScaleSeats(provider, organization.PlanType, organization.Seats!.Value) + ); + + await eventService.LogProviderOrganizationEventAsync( + providerOrganization, + EventType.ProviderOrganization_Added); + } + public async Task ChangePlan(ChangeProviderPlanCommand command) { var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId); @@ -206,6 +293,81 @@ public class ProviderBillingService( return memoryStream.ToArray(); } + [RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)] + public async Task> GetAddableOrganizations( + Provider provider, + Guid userId) + { + var providerUser = await providerUserRepository.GetByProviderUserAsync(provider.Id, userId); + + if (providerUser is not { Status: ProviderUserStatusType.Confirmed }) + { + throw new UnauthorizedAccessException(); + } + + var candidates = await organizationRepository.GetAddableToProviderByUserIdAsync(userId, provider.Type); + + var active = (await Task.WhenAll(candidates.Select(async organization => + { + var subscription = await subscriberService.GetSubscription(organization); + return (organization, subscription); + }))) + .Where(pair => pair.subscription is + { + Status: + StripeConstants.SubscriptionStatus.Active or + StripeConstants.SubscriptionStatus.Trialing or + StripeConstants.SubscriptionStatus.PastDue + }).ToList(); + + if (active.Count == 0) + { + return []; + } + + return await Task.WhenAll(active.Select(async pair => + { + var (organization, _) = pair; + + var planName = DerivePlanName(provider, organization); + + var addable = new AddableOrganization( + organization.Id, + organization.Name, + planName, + organization.Seats!.Value); + + if (providerUser.Type != ProviderUserType.ServiceUser) + { + return addable; + } + + var applicablePlanType = await GetManagedPlanTypeAsync(provider, organization); + + var requiresPurchase = + await SeatAdjustmentResultsInPurchase(provider, applicablePlanType, organization.Seats!.Value); + + return addable with { Disabled = requiresPurchase }; + })); + + string DerivePlanName(Provider localProvider, Organization localOrganization) + { + if (localProvider.Type == ProviderType.Msp) + { + return localOrganization.PlanType switch + { + var planType when PlanConstants.EnterprisePlanTypes.Contains(planType) => "Enterprise", + var planType when PlanConstants.TeamsPlanTypes.Contains(planType) => "Teams", + _ => throw new BillingException() + }; + } + + // TODO: Replace with PricingClient + var plan = StaticStore.GetPlan(localOrganization.PlanType); + return plan.Name; + } + } + public async Task ScaleSeats( Provider provider, PlanType planType, @@ -582,4 +744,21 @@ public class ProviderBillingService( return providerPlan; } + + private async Task GetManagedPlanTypeAsync( + Provider provider, + Organization organization) + { + if (provider.Type == ProviderType.MultiOrganizationEnterprise) + { + return (await providerPlanRepository.GetByProviderId(provider.Id)).First().PlanType; + } + + return organization.PlanType switch + { + var planType when PlanConstants.TeamsPlanTypes.Contains(planType) => PlanType.TeamsMonthly, + var planType when PlanConstants.EnterprisePlanTypes.Contains(planType) => PlanType.EnterpriseMonthly, + _ => throw new BillingException() + }; + } } diff --git a/src/Api/Billing/Controllers/ProviderClientsController.cs b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs similarity index 67% rename from src/Api/Billing/Controllers/ProviderClientsController.cs rename to src/Api/AdminConsole/Controllers/ProviderClientsController.cs index 0c09fa7baf..38d8b254d7 100644 --- a/src/Api/Billing/Controllers/ProviderClientsController.cs +++ b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs @@ -1,4 +1,6 @@ -using Bit.Api.Billing.Models.Requests; +using Bit.Api.Billing.Controllers; +using Bit.Api.Billing.Models.Requests; +using Bit.Core; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Services; @@ -7,13 +9,15 @@ using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Utilities; using Microsoft.AspNetCore.Mvc; -namespace Bit.Api.Billing.Controllers; +namespace Bit.Api.AdminConsole.Controllers; [Route("providers/{providerId:guid}/clients")] public class ProviderClientsController( ICurrentContext currentContext, + IFeatureService featureService, ILogger logger, IOrganizationRepository organizationRepository, IProviderBillingService providerBillingService, @@ -22,7 +26,10 @@ public class ProviderClientsController( IProviderService providerService, IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService) { + private readonly ICurrentContext _currentContext = currentContext; + [HttpPost] + [SelfHosted(NotSelfHostedOnly = true)] public async Task CreateAsync( [FromRoute] Guid providerId, [FromBody] CreateClientOrganizationRequestBody requestBody) @@ -80,6 +87,7 @@ public class ProviderClientsController( } [HttpPut("{providerOrganizationId:guid}")] + [SelfHosted(NotSelfHostedOnly = true)] public async Task UpdateAsync( [FromRoute] Guid providerId, [FromRoute] Guid providerOrganizationId, @@ -113,7 +121,7 @@ public class ProviderClientsController( clientOrganization.PlanType, seatAdjustment); - if (seatAdjustmentResultsInPurchase && !currentContext.ProviderProviderAdmin(provider.Id)) + if (seatAdjustmentResultsInPurchase && !_currentContext.ProviderProviderAdmin(provider.Id)) { return Error.Unauthorized("Service users cannot purchase additional seats."); } @@ -127,4 +135,58 @@ public class ProviderClientsController( return TypedResults.Ok(); } + + [HttpGet("addable")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task GetAddableOrganizationsAsync([FromRoute] Guid providerId) + { + if (!featureService.IsEnabled(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)) + { + return Error.NotFound(); + } + + var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId); + + if (provider == null) + { + return result; + } + + var userId = _currentContext.UserId; + + if (!userId.HasValue) + { + return Error.Unauthorized(); + } + + var addable = + await providerBillingService.GetAddableOrganizations(provider, userId.Value); + + return TypedResults.Ok(addable); + } + + [HttpPost("existing")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task AddExistingOrganizationAsync( + [FromRoute] Guid providerId, + [FromBody] AddExistingOrganizationRequestBody requestBody) + { + var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId); + + if (provider == null) + { + return result; + } + + var organization = await organizationRepository.GetByIdAsync(requestBody.OrganizationId); + + if (organization == null) + { + return Error.BadRequest("The organization being added to the provider does not exist."); + } + + await providerBillingService.AddExistingOrganization(provider, organization, requestBody.Key); + + return TypedResults.Ok(); + } } diff --git a/src/Api/Billing/Models/Requests/AddExistingOrganizationRequestBody.cs b/src/Api/Billing/Models/Requests/AddExistingOrganizationRequestBody.cs new file mode 100644 index 0000000000..c2add17793 --- /dev/null +++ b/src/Api/Billing/Models/Requests/AddExistingOrganizationRequestBody.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.Billing.Models.Requests; + +public class AddExistingOrganizationRequestBody +{ + [Required(ErrorMessage = "'key' must be provided")] + public string Key { get; set; } + + [Required(ErrorMessage = "'organizationId' must be provided")] + public Guid OrganizationId { get; set; } +} diff --git a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs index 5b274d3f88..584d95ffe2 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Models.Data.Organizations; #nullable enable @@ -22,4 +23,5 @@ public interface IOrganizationRepository : IRepository /// Gets the organizations that have a verified domain matching the user's email domain. ///
Task> GetByVerifiedUserEmailDomainAsync(Guid userId); + Task> GetAddableToProviderByUserIdAsync(Guid userId, ProviderType providerType); } diff --git a/src/Core/Billing/Constants/PlanConstants.cs b/src/Core/Billing/Constants/PlanConstants.cs new file mode 100644 index 0000000000..1ac5b8e750 --- /dev/null +++ b/src/Core/Billing/Constants/PlanConstants.cs @@ -0,0 +1,30 @@ +using Bit.Core.Billing.Enums; + +namespace Bit.Core.Billing.Constants; + +public static class PlanConstants +{ + public static List EnterprisePlanTypes => + [ + PlanType.EnterpriseAnnually2019, + PlanType.EnterpriseAnnually2020, + PlanType.EnterpriseAnnually2023, + PlanType.EnterpriseAnnually, + PlanType.EnterpriseMonthly2019, + PlanType.EnterpriseMonthly2020, + PlanType.EnterpriseMonthly2023, + PlanType.EnterpriseMonthly + ]; + + public static List TeamsPlanTypes => + [ + PlanType.TeamsAnnually2019, + PlanType.TeamsAnnually2020, + PlanType.TeamsAnnually2023, + PlanType.TeamsAnnually, + PlanType.TeamsMonthly2019, + PlanType.TeamsMonthly2020, + PlanType.TeamsMonthly2023, + PlanType.TeamsMonthly + ]; +} diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs index 7371b8b7e9..e3c2b7245e 100644 --- a/src/Core/Billing/Constants/StripeConstants.cs +++ b/src/Core/Billing/Constants/StripeConstants.cs @@ -31,6 +31,16 @@ public static class StripeConstants public const string TaxIdInvalid = "tax_id_invalid"; } + public static class InvoiceStatus + { + public const string Draft = "draft"; + } + + public static class MetadataKeys + { + public const string OrganizationId = "organizationId"; + } + public static class PaymentBehavior { public const string DefaultIncomplete = "default_incomplete"; diff --git a/src/Core/Billing/Models/AddableOrganization.cs b/src/Core/Billing/Models/AddableOrganization.cs new file mode 100644 index 0000000000..fe6d5458bd --- /dev/null +++ b/src/Core/Billing/Models/AddableOrganization.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Billing.Models; + +public record AddableOrganization( + Guid Id, + string Name, + string Plan, + int Seats, + bool Disabled = false); diff --git a/src/Core/Billing/Services/IProviderBillingService.cs b/src/Core/Billing/Services/IProviderBillingService.cs index 20e7407628..d6983da03e 100644 --- a/src/Core/Billing/Services/IProviderBillingService.cs +++ b/src/Core/Billing/Services/IProviderBillingService.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models; using Bit.Core.Billing.Services.Contracts; using Bit.Core.Models.Business; using Stripe; @@ -10,6 +11,11 @@ namespace Bit.Core.Billing.Services; public interface IProviderBillingService { + Task AddExistingOrganization( + Provider provider, + Organization organization, + string key); + /// /// Changes the assigned provider plan for the provider. /// @@ -35,6 +41,10 @@ public interface IProviderBillingService Task GenerateClientInvoiceReport( string invoiceId); + Task> GetAddableOrganizations( + Provider provider, + Guid userId); + /// /// Scales the 's seats for the specified using the provided . /// This operation may autoscale the provider's Stripe depending on the 's seat minimum for the diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index b196306409..8660010871 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -172,6 +172,7 @@ public static class FeatureFlagKeys public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication"; public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications"; public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync"; + public const string P15179_AddExistingOrgsFromProviderPortal = "PM-15179-add-existing-orgs-from-provider-portal"; public static List GetAllKeys() { diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs index 20fdf83155..f624f7da28 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs @@ -1,5 +1,6 @@ using System.Data; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Auth.Entities; using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations; @@ -180,4 +181,19 @@ public class OrganizationRepository : Repository, IOrganizat return result.ToList(); } } + + public async Task> GetAddableToProviderByUserIdAsync( + Guid userId, + ProviderType providerType) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var result = await connection.QueryAsync( + $"[{Schema}].[{Table}_ReadAddableToProviderByUserId]", + new { UserId = userId, ProviderType = providerType }, + commandType: CommandType.StoredProcedure); + + return result.ToList(); + } + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index c1c78eee60..b6ec2ddca0 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -1,9 +1,12 @@ using AutoMapper; using AutoMapper.QueryableExtensions; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; +using LinqToDB.Tools; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -298,6 +301,41 @@ public class OrganizationRepository : Repository> GetAddableToProviderByUserIdAsync( + Guid userId, + ProviderType providerType) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + + var planTypes = providerType switch + { + ProviderType.Msp => PlanConstants.EnterprisePlanTypes.Concat(PlanConstants.TeamsPlanTypes), + ProviderType.MultiOrganizationEnterprise => PlanConstants.EnterprisePlanTypes, + _ => [] + }; + + var query = + from organizationUser in dbContext.OrganizationUsers + join organization in dbContext.Organizations on organizationUser.OrganizationId equals organization.Id + where + organizationUser.UserId == userId && + organizationUser.Type == OrganizationUserType.Owner && + organizationUser.Status == OrganizationUserStatusType.Confirmed && + organization.Enabled && + organization.GatewayCustomerId != null && + organization.GatewaySubscriptionId != null && + organization.Seats > 0 && + organization.Status == OrganizationStatusType.Created && + !organization.UseSecretsManager && + organization.PlanType.In(planTypes) + select organization; + + return await query.ToArrayAsync(); + } + } + public Task EnableCollectionEnhancements(Guid organizationId) { throw new NotImplementedException("Collection enhancements migration is not yet supported for Entity Framework."); diff --git a/src/Sql/dbo/Stored Procedures/Organization_ReadAddableToProviderByUserId.sql b/src/Sql/dbo/Stored Procedures/Organization_ReadAddableToProviderByUserId.sql new file mode 100644 index 0000000000..e11109ae10 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Organization_ReadAddableToProviderByUserId.sql @@ -0,0 +1,23 @@ +CREATE PROCEDURE [dbo].[Organization_ReadAddableToProviderByUserId] + @UserId UNIQUEIDENTIFIER, + @ProviderType TINYINT +AS +BEGIN + SET NOCOUNT ON + SELECT O.* FROM [dbo].[OrganizationUser] AS OU + JOIN [dbo].[Organization] AS O ON O.[Id] = OU.[OrganizationId] + WHERE + OU.[UserId] = @UserId AND + OU.[Type] = 0 AND + OU.[Status] = 2 AND + O.[Enabled] = 1 AND + O.[GatewayCustomerId] IS NOT NULL AND + O.[GatewaySubscriptionId] IS NOT NULL AND + O.[Seats] > 0 AND + O.[Status] = 1 AND + O.[UseSecretsManager] = 0 AND + -- All Teams & Enterprise for MSP + (@ProviderType = 0 AND O.[PlanType] IN (2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20) OR + -- All Enterprise for MOE + @ProviderType = 2 AND O.[PlanType] IN (4, 5, 10, 11, 14, 15, 19, 20)); +END diff --git a/test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs similarity index 98% rename from test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs rename to test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs index 86bacd9aa3..8ddd92a5fa 100644 --- a/test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs @@ -1,5 +1,5 @@ using System.Security.Claims; -using Bit.Api.Billing.Controllers; +using Bit.Api.AdminConsole.Controllers; using Bit.Api.Billing.Models.Requests; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; @@ -19,10 +19,9 @@ using Microsoft.AspNetCore.Http.HttpResults; using NSubstitute; using NSubstitute.ReturnsExtensions; using Xunit; - using static Bit.Api.Test.Billing.Utilities; -namespace Bit.Api.Test.Billing.Controllers; +namespace Bit.Api.Test.AdminConsole.Controllers; [ControllerCustomize(typeof(ProviderClientsController))] [SutProviderCustomize] diff --git a/util/Migrator/DbScripts/2025-01-28_00_Add_Organization_ReadAddableToProviderByUserId.sql b/util/Migrator/DbScripts/2025-01-28_00_Add_Organization_ReadAddableToProviderByUserId.sql new file mode 100644 index 0000000000..1255544d19 --- /dev/null +++ b/util/Migrator/DbScripts/2025-01-28_00_Add_Organization_ReadAddableToProviderByUserId.sql @@ -0,0 +1,31 @@ +-- Drop existing SPROC +IF OBJECT_ID('[dbo].[Organization_ReadAddableToProviderByUserId') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Organization_ReadAddableToProviderByUserId] +END +GO + +CREATE PROCEDURE [dbo].[Organization_ReadAddableToProviderByUserId] + @UserId UNIQUEIDENTIFIER, + @ProviderType TINYINT +AS +BEGIN + SET NOCOUNT ON + SELECT O.* FROM [dbo].[OrganizationUser] AS OU + JOIN [dbo].[Organization] AS O ON O.[Id] = OU.[OrganizationId] + WHERE + OU.[UserId] = @UserId AND + OU.[Type] = 0 AND + OU.[Status] = 2 AND + O.[Enabled] = 1 AND + O.[GatewayCustomerId] IS NOT NULL AND + O.[GatewaySubscriptionId] IS NOT NULL AND + O.[Seats] > 0 AND + O.[Status] = 1 AND + O.[UseSecretsManager] = 0 AND + -- All Teams & Enterprise for MSP + (@ProviderType = 0 AND O.[PlanType] IN (2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20) OR + -- All Enterprise for MOE + @ProviderType = 2 AND O.[PlanType] IN (4, 5, 10, 11, 14, 15, 19, 20)); +END +GO From 3f3da558b6c47a91c09f32e3535451d58f4858ee Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Tue, 4 Feb 2025 08:02:43 -0600 Subject: [PATCH 089/148] [PM-17562] Refactor existing RabbitMq implementation (#5357) * [PM-17562] Refactor existing RabbitMq implementation * Fixed issues noted in PR review --- .../Services/IEventMessageHandler.cs | 8 +++ .../Implementations/EventRepositoryHandler.cs | 14 ++++ ...ostListener.cs => HttpPostEventHandler.cs} | 15 ++--- .../RabbitMqEventRepositoryListener.cs | 29 -------- .../Services/EventLoggingListenerService.cs | 13 ++++ .../RabbitMqEventListenerService.cs} | 27 ++++---- src/Core/Settings/IGlobalSettings.cs | 1 + src/Events/Startup.cs | 19 +++++- .../MockedHttpMessageHandler.cs | 3 + .../Services/EventRepositoryHandlerTests.cs | 24 +++++++ .../Services/HttpPostEventHandlerTests.cs | 66 +++++++++++++++++++ 11 files changed, 162 insertions(+), 57 deletions(-) create mode 100644 src/Core/AdminConsole/Services/IEventMessageHandler.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs rename src/Core/AdminConsole/Services/Implementations/{RabbitMqEventHttpPostListener.cs => HttpPostEventHandler.cs} (52%) delete mode 100644 src/Core/AdminConsole/Services/Implementations/RabbitMqEventRepositoryListener.cs create mode 100644 src/Core/Services/EventLoggingListenerService.cs rename src/Core/{AdminConsole/Services/Implementations/RabbitMqEventListenerBase.cs => Services/Implementations/RabbitMqEventListenerService.cs} (78%) create mode 100644 test/Core.Test/AdminConsole/Services/EventRepositoryHandlerTests.cs create mode 100644 test/Core.Test/AdminConsole/Services/HttpPostEventHandlerTests.cs diff --git a/src/Core/AdminConsole/Services/IEventMessageHandler.cs b/src/Core/AdminConsole/Services/IEventMessageHandler.cs new file mode 100644 index 0000000000..5df9544c29 --- /dev/null +++ b/src/Core/AdminConsole/Services/IEventMessageHandler.cs @@ -0,0 +1,8 @@ +using Bit.Core.Models.Data; + +namespace Bit.Core.Services; + +public interface IEventMessageHandler +{ + Task HandleEventAsync(EventMessage eventMessage); +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs new file mode 100644 index 0000000000..6e4158122c --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs @@ -0,0 +1,14 @@ +using Bit.Core.Models.Data; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Services; + +public class EventRepositoryHandler( + [FromKeyedServices("persistent")] IEventWriteService eventWriteService) + : IEventMessageHandler +{ + public Task HandleEventAsync(EventMessage eventMessage) + { + return eventWriteService.CreateAsync(eventMessage); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventHttpPostListener.cs b/src/Core/AdminConsole/Services/Implementations/HttpPostEventHandler.cs similarity index 52% rename from src/Core/AdminConsole/Services/Implementations/RabbitMqEventHttpPostListener.cs rename to src/Core/AdminConsole/Services/Implementations/HttpPostEventHandler.cs index 5a875f9278..8aece0c1da 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventHttpPostListener.cs +++ b/src/Core/AdminConsole/Services/Implementations/HttpPostEventHandler.cs @@ -1,32 +1,25 @@ using System.Net.Http.Json; using Bit.Core.Models.Data; using Bit.Core.Settings; -using Microsoft.Extensions.Logging; namespace Bit.Core.Services; -public class RabbitMqEventHttpPostListener : RabbitMqEventListenerBase +public class HttpPostEventHandler : IEventMessageHandler { private readonly HttpClient _httpClient; private readonly string _httpPostUrl; - private readonly string _queueName; - protected override string QueueName => _queueName; + public const string HttpClientName = "HttpPostEventHandlerHttpClient"; - public const string HttpClientName = "EventHttpPostListenerHttpClient"; - - public RabbitMqEventHttpPostListener( + public HttpPostEventHandler( IHttpClientFactory httpClientFactory, - ILogger logger, GlobalSettings globalSettings) - : base(logger, globalSettings) { _httpClient = httpClientFactory.CreateClient(HttpClientName); _httpPostUrl = globalSettings.EventLogging.RabbitMq.HttpPostUrl; - _queueName = globalSettings.EventLogging.RabbitMq.HttpPostQueueName; } - protected override async Task HandleMessageAsync(EventMessage eventMessage) + public async Task HandleEventAsync(EventMessage eventMessage) { var content = JsonContent.Create(eventMessage); var response = await _httpClient.PostAsync(_httpPostUrl, content); diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventRepositoryListener.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventRepositoryListener.cs deleted file mode 100644 index 25d85bddeb..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventRepositoryListener.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Bit.Core.Models.Data; -using Bit.Core.Settings; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Bit.Core.Services; - -public class RabbitMqEventRepositoryListener : RabbitMqEventListenerBase -{ - private readonly IEventWriteService _eventWriteService; - private readonly string _queueName; - - protected override string QueueName => _queueName; - - public RabbitMqEventRepositoryListener( - [FromKeyedServices("persistent")] IEventWriteService eventWriteService, - ILogger logger, - GlobalSettings globalSettings) - : base(logger, globalSettings) - { - _eventWriteService = eventWriteService; - _queueName = globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName; - } - - protected override Task HandleMessageAsync(EventMessage eventMessage) - { - return _eventWriteService.CreateAsync(eventMessage); - } -} diff --git a/src/Core/Services/EventLoggingListenerService.cs b/src/Core/Services/EventLoggingListenerService.cs new file mode 100644 index 0000000000..60b8789a6b --- /dev/null +++ b/src/Core/Services/EventLoggingListenerService.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Hosting; + +namespace Bit.Core.Services; + +public abstract class EventLoggingListenerService : BackgroundService +{ + protected readonly IEventMessageHandler _handler; + + protected EventLoggingListenerService(IEventMessageHandler handler) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerBase.cs b/src/Core/Services/Implementations/RabbitMqEventListenerService.cs similarity index 78% rename from src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerBase.cs rename to src/Core/Services/Implementations/RabbitMqEventListenerService.cs index 48a549d261..9360170368 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerBase.cs +++ b/src/Core/Services/Implementations/RabbitMqEventListenerService.cs @@ -1,26 +1,26 @@ using System.Text.Json; using Bit.Core.Models.Data; using Bit.Core.Settings; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using RabbitMQ.Client; using RabbitMQ.Client.Events; namespace Bit.Core.Services; -public abstract class RabbitMqEventListenerBase : BackgroundService +public class RabbitMqEventListenerService : EventLoggingListenerService { private IChannel _channel; private IConnection _connection; private readonly string _exchangeName; private readonly ConnectionFactory _factory; - private readonly ILogger _logger; + private readonly ILogger _logger; + private readonly string _queueName; - protected abstract string QueueName { get; } - - protected RabbitMqEventListenerBase( - ILogger logger, - GlobalSettings globalSettings) + public RabbitMqEventListenerService( + IEventMessageHandler handler, + ILogger logger, + GlobalSettings globalSettings, + string queueName) : base(handler) { _factory = new ConnectionFactory { @@ -30,6 +30,7 @@ public abstract class RabbitMqEventListenerBase : BackgroundService }; _exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName; _logger = logger; + _queueName = queueName; } public override async Task StartAsync(CancellationToken cancellationToken) @@ -38,13 +39,13 @@ public abstract class RabbitMqEventListenerBase : BackgroundService _channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken); await _channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true); - await _channel.QueueDeclareAsync(queue: QueueName, + await _channel.QueueDeclareAsync(queue: _queueName, durable: true, exclusive: false, autoDelete: false, arguments: null, cancellationToken: cancellationToken); - await _channel.QueueBindAsync(queue: QueueName, + await _channel.QueueBindAsync(queue: _queueName, exchange: _exchangeName, routingKey: string.Empty, cancellationToken: cancellationToken); @@ -59,7 +60,7 @@ public abstract class RabbitMqEventListenerBase : BackgroundService try { var eventMessage = JsonSerializer.Deserialize(eventArgs.Body.Span); - await HandleMessageAsync(eventMessage); + await _handler.HandleEventAsync(eventMessage); } catch (Exception ex) { @@ -67,7 +68,7 @@ public abstract class RabbitMqEventListenerBase : BackgroundService } }; - await _channel.BasicConsumeAsync(QueueName, autoAck: true, consumer: consumer, cancellationToken: stoppingToken); + await _channel.BasicConsumeAsync(_queueName, autoAck: true, consumer: consumer, cancellationToken: stoppingToken); while (!stoppingToken.IsCancellationRequested) { @@ -88,6 +89,4 @@ public abstract class RabbitMqEventListenerBase : BackgroundService _connection.Dispose(); base.Dispose(); } - - protected abstract Task HandleMessageAsync(EventMessage eventMessage); } diff --git a/src/Core/Settings/IGlobalSettings.cs b/src/Core/Settings/IGlobalSettings.cs index afe35ed34b..b89df8abf5 100644 --- a/src/Core/Settings/IGlobalSettings.cs +++ b/src/Core/Settings/IGlobalSettings.cs @@ -27,4 +27,5 @@ public interface IGlobalSettings string DatabaseProvider { get; set; } GlobalSettings.SqlSettings SqlServer { get; set; } string DevelopmentDirectory { get; set; } + GlobalSettings.EventLoggingSettings EventLogging { get; set; } } diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index 03e99f14e8..b692733a55 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -89,13 +89,26 @@ public class Startup CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) && CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName)) { + services.AddSingleton(); services.AddKeyedSingleton("persistent"); - services.AddHostedService(); + services.AddSingleton(provider => + new RabbitMqEventListenerService( + provider.GetRequiredService(), + provider.GetRequiredService>(), + provider.GetRequiredService(), + globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName)); if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HttpPostUrl)) { - services.AddHttpClient(RabbitMqEventHttpPostListener.HttpClientName); - services.AddHostedService(); + services.AddSingleton(); + services.AddHttpClient(HttpPostEventHandler.HttpClientName); + + services.AddSingleton(provider => + new RabbitMqEventListenerService( + provider.GetRequiredService(), + provider.GetRequiredService>(), + provider.GetRequiredService(), + globalSettings.EventLogging.RabbitMq.HttpPostQueueName)); } } } diff --git a/test/Common/MockedHttpClient/MockedHttpMessageHandler.cs b/test/Common/MockedHttpClient/MockedHttpMessageHandler.cs index 1b1bd52a03..8a6c1dae97 100644 --- a/test/Common/MockedHttpClient/MockedHttpMessageHandler.cs +++ b/test/Common/MockedHttpClient/MockedHttpMessageHandler.cs @@ -8,6 +8,8 @@ public class MockedHttpMessageHandler : HttpMessageHandler { private readonly List _matchers = new(); + public List CapturedRequests { get; } = new List(); + /// /// The fallback handler to use when the request does not match any of the provided matchers. /// @@ -16,6 +18,7 @@ public class MockedHttpMessageHandler : HttpMessageHandler protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + CapturedRequests.Add(request); var matcher = _matchers.FirstOrDefault(x => x.Matches(request)); if (matcher == null) { diff --git a/test/Core.Test/AdminConsole/Services/EventRepositoryHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventRepositoryHandlerTests.cs new file mode 100644 index 0000000000..2b143f5cb8 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/EventRepositoryHandlerTests.cs @@ -0,0 +1,24 @@ +using Bit.Core.Models.Data; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class EventRepositoryHandlerTests +{ + [Theory, BitAutoData] + public async Task HandleEventAsync_WritesEventToIEventWriteService( + EventMessage eventMessage, + SutProvider sutProvider) + { + await sutProvider.Sut.HandleEventAsync(eventMessage); + await sutProvider.GetDependency().Received(1).CreateAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(eventMessage)) + ); + } +} diff --git a/test/Core.Test/AdminConsole/Services/HttpPostEventHandlerTests.cs b/test/Core.Test/AdminConsole/Services/HttpPostEventHandlerTests.cs new file mode 100644 index 0000000000..414b1c54be --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/HttpPostEventHandlerTests.cs @@ -0,0 +1,66 @@ +using System.Net; +using System.Net.Http.Json; +using Bit.Core.Models.Data; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using Bit.Test.Common.MockedHttpClient; +using NSubstitute; +using Xunit; +using GlobalSettings = Bit.Core.Settings.GlobalSettings; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class HttpPostEventHandlerTests +{ + private readonly MockedHttpMessageHandler _handler; + private HttpClient _httpClient; + + private const string _httpPostUrl = "http://localhost/test/event"; + + public HttpPostEventHandlerTests() + { + _handler = new MockedHttpMessageHandler(); + _handler.Fallback + .WithStatusCode(HttpStatusCode.OK) + .WithContent(new StringContent("testtest")); + _httpClient = _handler.ToHttpClient(); + } + + public SutProvider GetSutProvider() + { + var clientFactory = Substitute.For(); + clientFactory.CreateClient(HttpPostEventHandler.HttpClientName).Returns(_httpClient); + + var globalSettings = new GlobalSettings(); + globalSettings.EventLogging.RabbitMq.HttpPostUrl = _httpPostUrl; + + return new SutProvider() + .SetDependency(globalSettings) + .SetDependency(clientFactory) + .Create(); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_PostsEventsToUrl(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(); + var content = JsonContent.Create(eventMessage); + + await sutProvider.Sut.HandleEventAsync(eventMessage); + sutProvider.GetDependency().Received(1).CreateClient( + Arg.Is(AssertHelper.AssertPropertyEqual(HttpPostEventHandler.HttpClientName)) + ); + + Assert.Single(_handler.CapturedRequests); + var request = _handler.CapturedRequests[0]; + Assert.NotNull(request); + var returned = await request.Content.ReadFromJsonAsync(); + + Assert.Equal(HttpMethod.Post, request.Method); + Assert.Equal(_httpPostUrl, request.RequestUri.ToString()); + AssertHelper.AssertPropertyEqual(eventMessage, returned, new[] { "IdempotencyId" }); + } +} From 37b5cef085972c29ffd6e615b7d27a19ffde81ae Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 4 Feb 2025 09:06:04 -0500 Subject: [PATCH 090/148] [PM-16040] Update Organization_UnassignedToProviderSearch.sql SPROC to allow Reseller plan types (#5332) * Update Organization_UnassignedToProviderSearch.sql SPROC * Robert's feedback --- .../Repositories/OrganizationRepository.cs | 18 ++++--- ...rganization_UnassignedToProviderSearch.sql | 14 ++--- ...rganization_UnassignedToProviderSearch.sql | 54 +++++++++++++++++++ 3 files changed, 73 insertions(+), 13 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-01-28_00_UpdateOrganization_UnassignedToProviderSearch.sql diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index b6ec2ddca0..ea4e1334c6 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -117,13 +117,19 @@ public class OrganizationRepository : Repository + { + PlanType.Free, + PlanType.Custom, + PlanType.FamiliesAnnually2019, + PlanType.FamiliesAnnually + }; + var query = from o in dbContext.Organizations - where - ((o.PlanType >= PlanType.TeamsMonthly2019 && o.PlanType <= PlanType.EnterpriseAnnually2019) || - (o.PlanType >= PlanType.TeamsMonthly2020 && o.PlanType <= PlanType.EnterpriseAnnually)) && - !dbContext.ProviderOrganizations.Any(po => po.OrganizationId == o.Id) && - (string.IsNullOrWhiteSpace(name) || EF.Functions.Like(o.Name, $"%{name}%")) + where o.PlanType.NotIn(disallowedPlanTypes) && + !dbContext.ProviderOrganizations.Any(po => po.OrganizationId == o.Id) && + (string.IsNullOrWhiteSpace(name) || EF.Functions.Like(o.Name, $"%{name}%")) select o; if (string.IsNullOrWhiteSpace(ownerEmail)) @@ -155,7 +161,7 @@ public class OrganizationRepository : Repository o.CreationDate).Skip(skip).Take(take).ToArrayAsync(); + return await query.OrderByDescending(o => o.CreationDate).ThenByDescending(o => o.Id).Skip(skip).Take(take).ToArrayAsync(); } public async Task UpdateStorageAsync(Guid id) diff --git a/src/Sql/dbo/Stored Procedures/Organization_UnassignedToProviderSearch.sql b/src/Sql/dbo/Stored Procedures/Organization_UnassignedToProviderSearch.sql index e40f78fee0..4f2269b583 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_UnassignedToProviderSearch.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_UnassignedToProviderSearch.sql @@ -1,5 +1,5 @@ CREATE PROCEDURE [dbo].[Organization_UnassignedToProviderSearch] - @Name NVARCHAR(50), + @Name NVARCHAR(55), @OwnerEmail NVARCHAR(256), @Skip INT = 0, @Take INT = 25 @@ -9,7 +9,7 @@ BEGIN SET NOCOUNT ON DECLARE @NameLikeSearch NVARCHAR(55) = '%' + @Name + '%' DECLARE @OwnerLikeSearch NVARCHAR(55) = @OwnerEmail + '%' - + IF @OwnerEmail IS NOT NULL BEGIN SELECT @@ -21,11 +21,11 @@ BEGIN INNER JOIN [dbo].[User] U ON U.[Id] = OU.[UserId] WHERE - ((O.[PlanType] >= 2 AND O.[PlanType] <= 5) OR (O.[PlanType] >= 8 AND O.[PlanType] <= 20) AND (O.PlanType <> 16)) -- All 'Teams' and 'Enterprise' organizations + O.[PlanType] NOT IN (0, 1, 6, 7) -- Not 'Free', 'Custom' or 'Families' AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id]) AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch) AND (U.[Email] LIKE @OwnerLikeSearch) - ORDER BY O.[CreationDate] DESC + ORDER BY O.[CreationDate] DESC, O.[Id] OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY END @@ -36,11 +36,11 @@ BEGIN FROM [dbo].[OrganizationView] O WHERE - ((O.[PlanType] >= 2 AND O.[PlanType] <= 5) OR (O.[PlanType] >= 8 AND O.[PlanType] <= 20) AND (O.PlanType <> 16)) -- All 'Teams' and 'Enterprise' organizations + O.[PlanType] NOT IN (0, 1, 6, 7) -- Not 'Free', 'Custom' or 'Families' AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id]) AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch) - ORDER BY O.[CreationDate] DESC + ORDER BY O.[CreationDate] DESC, O.[Id] OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY END -END \ No newline at end of file +END diff --git a/util/Migrator/DbScripts/2025-01-28_00_UpdateOrganization_UnassignedToProviderSearch.sql b/util/Migrator/DbScripts/2025-01-28_00_UpdateOrganization_UnassignedToProviderSearch.sql new file mode 100644 index 0000000000..07ec9ae8ac --- /dev/null +++ b/util/Migrator/DbScripts/2025-01-28_00_UpdateOrganization_UnassignedToProviderSearch.sql @@ -0,0 +1,54 @@ +-- Drop existing SPROC +IF OBJECT_ID('[dbo].[Organization_UnassignedToProviderSearch]') IS NOT NULL + BEGIN + DROP PROCEDURE [dbo].[Organization_UnassignedToProviderSearch] + END +GO + +CREATE PROCEDURE [dbo].[Organization_UnassignedToProviderSearch] + @Name NVARCHAR(55), + @OwnerEmail NVARCHAR(256), + @Skip INT = 0, + @Take INT = 25 + WITH RECOMPILE +AS +BEGIN + SET NOCOUNT ON + DECLARE @NameLikeSearch NVARCHAR(55) = '%' + @Name + '%' + DECLARE @OwnerLikeSearch NVARCHAR(55) = @OwnerEmail + '%' + + IF @OwnerEmail IS NOT NULL + BEGIN + SELECT + O.* + FROM + [dbo].[OrganizationView] O + INNER JOIN + [dbo].[OrganizationUser] OU ON O.[Id] = OU.[OrganizationId] + INNER JOIN + [dbo].[User] U ON U.[Id] = OU.[UserId] + WHERE + O.[PlanType] NOT IN (0, 1, 6, 7) -- Not 'Free', 'Custom' or 'Families' + AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id]) + AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch) + AND (U.[Email] LIKE @OwnerLikeSearch) + ORDER BY O.[CreationDate] DESC, O.[Id] + OFFSET @Skip ROWS + FETCH NEXT @Take ROWS ONLY + END + ELSE + BEGIN + SELECT + O.* + FROM + [dbo].[OrganizationView] O + WHERE + O.[PlanType] NOT IN (0, 1, 6, 7) -- Not 'Free', 'Custom' or 'Families' + AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id]) + AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch) + ORDER BY O.[CreationDate] DESC, O.[Id] + OFFSET @Skip ROWS + FETCH NEXT @Take ROWS ONLY + END +END +GO From 0337300eac108ad2965795758febd895f93781e2 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:27:58 +0100 Subject: [PATCH 091/148] [PM-15625]Disable trial/send-verification-email endpoint for self-host (#5265) * endpoint is shut off for self-hosted env Signed-off-by: Cy Okeke * Fix the reference issues Signed-off-by: Cy Okeke --------- Signed-off-by: Cy Okeke --- src/Identity/Billing/Controller/AccountsController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Identity/Billing/Controller/AccountsController.cs b/src/Identity/Billing/Controller/AccountsController.cs index aada40bcb2..96ec1280cd 100644 --- a/src/Identity/Billing/Controller/AccountsController.cs +++ b/src/Identity/Billing/Controller/AccountsController.cs @@ -4,6 +4,7 @@ using Bit.Core.Context; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; +using Bit.Core.Utilities; using Bit.SharedWeb.Utilities; using Microsoft.AspNetCore.Mvc; @@ -17,6 +18,7 @@ public class AccountsController( IReferenceEventService referenceEventService) : Microsoft.AspNetCore.Mvc.Controller { [HttpPost("trial/send-verification-email")] + [SelfHosted(NotSelfHostedOnly = true)] public async Task PostTrialInitiationSendVerificationEmailAsync([FromBody] TrialSendVerificationEmailRequestModel model) { var token = await sendTrialInitiationEmailForRegistrationCommand.Handle( From b5cfb4b9c73de206999c170519fde76a1fc86682 Mon Sep 17 00:00:00 2001 From: Matt Andreko Date: Tue, 4 Feb 2025 12:14:55 -0500 Subject: [PATCH 092/148] Enabled SonarQube scanning for PRs (#5363) * Added scan workflow parameter for PR number to enable branch scanning * Added missing backslash --- .github/workflows/scan.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index ec2eb7789a..fbcff6b1c0 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -85,6 +85,7 @@ jobs: /d:sonar.test.inclusions=test/,bitwarden_license/test/ \ /d:sonar.exclusions=test/,bitwarden_license/test/ \ /o:"${{ github.repository_owner }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" \ + /d:sonar.pullrequest.key=${{ github.event.pull_request.number }} \ /d:sonar.host.url="https://sonarcloud.io" dotnet build dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" From bdbed7adc8ef7be9c6da07207405f2cc04366707 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 4 Feb 2025 19:31:15 +0100 Subject: [PATCH 093/148] Group tools owned feature flags (#5362) Co-authored-by: Daniel James Smith --- src/Core/Constants.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 8660010871..6f9960919a 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -109,9 +109,15 @@ public static class FeatureFlagKeys public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications"; public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission"; + /* Tools Team */ + public const string ItemShare = "item-share"; + public const string GeneratorToolsModernization = "generator-tools-modernization"; + public const string MemberAccessReport = "ac-2059-member-access-report"; + public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; + public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications"; + public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection"; - public const string ItemShare = "item-share"; public const string DuoRedirect = "duo-redirect"; public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email"; public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section"; @@ -121,7 +127,6 @@ public static class FeatureFlagKeys public const string RestrictProviderAccess = "restrict-provider-access"; public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service"; public const string VaultBulkManagementAction = "vault-bulk-management-action"; - public const string MemberAccessReport = "ac-2059-member-access-report"; public const string InlineMenuFieldQualification = "inline-menu-field-qualification"; public const string TwoFactorComponentRefactor = "two-factor-component-refactor"; public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements"; @@ -146,9 +151,7 @@ public static class FeatureFlagKeys public const string TrialPayment = "PM-8163-trial-payment"; public const string RemoveServerVersionHeader = "remove-server-version-header"; public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises"; - public const string GeneratorToolsModernization = "generator-tools-modernization"; public const string NewDeviceVerification = "new-device-verification"; - public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss"; public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss"; public const string SecurityTasks = "security-tasks"; @@ -170,7 +173,6 @@ public static class FeatureFlagKeys public const string AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner"; public const string SingleTapPasskeyCreation = "single-tap-passkey-creation"; public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication"; - public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications"; public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync"; public const string P15179_AddExistingOrgsFromProviderPortal = "PM-15179-add-existing-orgs-from-provider-portal"; From d2fb3760d3da1debd260ba190741ea360644a848 Mon Sep 17 00:00:00 2001 From: Matt Andreko Date: Tue, 4 Feb 2025 13:53:16 -0500 Subject: [PATCH 094/148] Reworked PR workflow logic to prevent missing parameter (#5367) --- .github/workflows/scan.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index fbcff6b1c0..1fa5c9587c 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -85,7 +85,6 @@ jobs: /d:sonar.test.inclusions=test/,bitwarden_license/test/ \ /d:sonar.exclusions=test/,bitwarden_license/test/ \ /o:"${{ github.repository_owner }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" \ - /d:sonar.pullrequest.key=${{ github.event.pull_request.number }} \ - /d:sonar.host.url="https://sonarcloud.io" + /d:sonar.host.url="https://sonarcloud.io" ${{ contains(github.event_name, 'pull_request') && format('/d:sonar.pullrequest.key={0}', github.event.pull_request.number) || '' }} dotnet build dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" From 72b78ed65506b7433e1a224c29fb66ebaf36f55f Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:58:54 -0500 Subject: [PATCH 095/148] Update feature flag name (#5364) --- src/Core/Constants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 6f9960919a..f58f1f7157 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -174,7 +174,7 @@ public static class FeatureFlagKeys public const string SingleTapPasskeyCreation = "single-tap-passkey-creation"; public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication"; public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync"; - public const string P15179_AddExistingOrgsFromProviderPortal = "PM-15179-add-existing-orgs-from-provider-portal"; + public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal"; public static List GetAllKeys() { From 90680f482a3b20b8d580056c6d136baab4cbf089 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:40:17 -0500 Subject: [PATCH 096/148] Revert version from 2025.1.5 to 2025.1.4 (#5369) --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index d109303a58..88b63d156c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.1.5 + 2025.1.4 Bit.$(MSBuildProjectName) enable @@ -64,4 +64,4 @@ - \ No newline at end of file + From 412c6f9849431a3d170a8009c86f2d7fa4ae3a57 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Tue, 4 Feb 2025 15:45:24 -0500 Subject: [PATCH 097/148] [PM-11162] Assign to Collection Permission Update (#4844) Only users with Manage/Edit permissions will be allowed to Assign To Collections. If the user has Can Edit Except Password the collections dropdown will be disabled. --------- Co-authored-by: Matt Bishop Co-authored-by: kejaeger <138028972+kejaeger@users.noreply.github.com> --- .../Vault/Controllers/CiphersController.cs | 57 ++++++++- .../Vault/Repositories/CipherRepository.cs | 2 +- .../Cipher/CipherDetails_ReadByIdUserId.sql | 38 +++++- .../CollectionCipher_UpdateCollections.sql | 56 ++++----- .../Controllers/CiphersControllerTests.cs | 1 + ...0_CollectionPermissionEditExceptPWPerm.sql | 118 ++++++++++++++++++ 6 files changed, 233 insertions(+), 39 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-02-04_00_CollectionPermissionEditExceptPWPerm.sql diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index c8ebb8c402..5a7d427963 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -424,6 +424,59 @@ public class CiphersController : Controller return false; } + /// + /// TODO: Move this to its own authorization handler or equivalent service - AC-2062 + /// + private async Task CanModifyCipherCollectionsAsync(Guid organizationId, IEnumerable cipherIds) + { + // If the user can edit all ciphers for the organization, just check they all belong to the org + if (await CanEditAllCiphersAsync(organizationId)) + { + // TODO: This can likely be optimized to only query the requested ciphers and then checking they belong to the org + var orgCiphers = (await _cipherRepository.GetManyByOrganizationIdAsync(organizationId)).ToDictionary(c => c.Id); + + // Ensure all requested ciphers are in orgCiphers + if (cipherIds.Any(c => !orgCiphers.ContainsKey(c))) + { + return false; + } + + return true; + } + + // The user cannot access any ciphers for the organization, we're done + if (!await CanAccessOrganizationCiphersAsync(organizationId)) + { + return false; + } + + var userId = _userService.GetProperUserId(User).Value; + // Select all editable ciphers for this user belonging to the organization + var editableOrgCipherList = (await _cipherRepository.GetManyByUserIdAsync(userId, true)) + .Where(c => c.OrganizationId == organizationId && c.UserId == null && c.Edit && c.ViewPassword).ToList(); + + // Special case for unassigned ciphers + if (await CanAccessUnassignedCiphersAsync(organizationId)) + { + var unassignedCiphers = + (await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync( + organizationId)); + + // Users that can access unassigned ciphers can also edit them + editableOrgCipherList.AddRange(unassignedCiphers.Select(c => new CipherDetails(c) { Edit = true })); + } + + var editableOrgCiphers = editableOrgCipherList + .ToDictionary(c => c.Id); + + if (cipherIds.Any(c => !editableOrgCiphers.ContainsKey(c))) + { + return false; + } + + return true; + } + /// /// TODO: Move this to its own authorization handler or equivalent service - AC-2062 /// @@ -579,7 +632,7 @@ public class CiphersController : Controller var userId = _userService.GetProperUserId(User).Value; var cipher = await GetByIdAsync(id, userId); if (cipher == null || !cipher.OrganizationId.HasValue || - !await _currentContext.OrganizationUser(cipher.OrganizationId.Value)) + !await _currentContext.OrganizationUser(cipher.OrganizationId.Value) || !cipher.ViewPassword) { throw new NotFoundException(); } @@ -634,7 +687,7 @@ public class CiphersController : Controller [HttpPost("bulk-collections")] public async Task PostBulkCollections([FromBody] CipherBulkUpdateCollectionsRequestModel model) { - if (!await CanEditCiphersAsync(model.OrganizationId, model.CipherIds) || + if (!await CanModifyCipherCollectionsAsync(model.OrganizationId, model.CipherIds) || !await CanEditItemsInCollections(model.OrganizationId, model.CollectionIds)) { throw new NotFoundException(); diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs index 098e8299e4..b8304fbbb0 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs @@ -98,7 +98,7 @@ public class CipherRepository : Repository, ICipherRepository return results .GroupBy(c => c.Id) - .Select(g => g.OrderByDescending(og => og.Edit).First()) + .Select(g => g.OrderByDescending(og => og.Edit).ThenByDescending(og => og.ViewPassword).First()) .ToList(); } } diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_ReadByIdUserId.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_ReadByIdUserId.sql index e2fb2629bd..189ad0a4a5 100644 --- a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_ReadByIdUserId.sql +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_ReadByIdUserId.sql @@ -5,12 +5,40 @@ AS BEGIN SET NOCOUNT ON - SELECT TOP 1 - * +SELECT + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [Attachments], + [CreationDate], + [RevisionDate], + [Favorite], + [FolderId], + [DeletedDate], + [Reprompt], + [Key], + [OrganizationUseTotp], + MAX ([Edit]) AS [Edit], + MAX ([ViewPassword]) AS [ViewPassword] FROM [dbo].[UserCipherDetails](@UserId) WHERE [Id] = @Id - ORDER BY - [Edit] DESC -END \ No newline at end of file + GROUP BY + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [Attachments], + [CreationDate], + [RevisionDate], + [Favorite], + [FolderId], + [DeletedDate], + [Reprompt], + [Key], + [OrganizationUseTotp] +END diff --git a/src/Sql/dbo/Stored Procedures/CollectionCipher_UpdateCollections.sql b/src/Sql/dbo/Stored Procedures/CollectionCipher_UpdateCollections.sql index 4098ab59e2..f3a1d964b5 100644 --- a/src/Sql/dbo/Stored Procedures/CollectionCipher_UpdateCollections.sql +++ b/src/Sql/dbo/Stored Procedures/CollectionCipher_UpdateCollections.sql @@ -14,10 +14,9 @@ BEGIN WHERE [Id] = @CipherId ) - - ;WITH [AvailableCollectionsCTE] AS( - SELECT + SELECT C.[Id] + INTO #TempAvailableCollections FROM [dbo].[Collection] C INNER JOIN @@ -40,38 +39,33 @@ BEGIN CU.[ReadOnly] = 0 OR CG.[ReadOnly] = 0 ) - ), - [CollectionCiphersCTE] AS( - SELECT - [CollectionId], - [CipherId] - FROM - [dbo].[CollectionCipher] - WHERE - [CipherId] = @CipherId + -- Insert new collection assignments + INSERT INTO [dbo].[CollectionCipher] ( + [CollectionId], + [CipherId] ) - MERGE - [CollectionCiphersCTE] AS [Target] - USING - @CollectionIds AS [Source] - ON - [Target].[CollectionId] = [Source].[Id] - AND [Target].[CipherId] = @CipherId - WHEN NOT MATCHED BY TARGET - AND [Source].[Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE]) THEN - INSERT VALUES - ( - [Source].[Id], - @CipherId - ) - WHEN NOT MATCHED BY SOURCE - AND [Target].[CipherId] = @CipherId - AND [Target].[CollectionId] IN (SELECT [Id] FROM [AvailableCollectionsCTE]) THEN - DELETE - ; + SELECT + [Id], + @CipherId + FROM @CollectionIds + WHERE [Id] IN (SELECT [Id] FROM [#TempAvailableCollections]) + AND NOT EXISTS ( + SELECT 1 + FROM [dbo].[CollectionCipher] + WHERE [CollectionId] = [@CollectionIds].[Id] + AND [CipherId] = @CipherId + ); + + -- Delete removed collection assignments + DELETE CC + FROM [dbo].[CollectionCipher] CC + WHERE CC.[CipherId] = @CipherId + AND CC.[CollectionId] IN (SELECT [Id] FROM [#TempAvailableCollections]) + AND CC.[CollectionId] NOT IN (SELECT [Id] FROM @CollectionIds); IF @OrgId IS NOT NULL BEGIN EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId END + DROP TABLE #TempAvailableCollections; END diff --git a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs index e7c5cd9ef5..2afce14ac5 100644 --- a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs @@ -127,6 +127,7 @@ public class CiphersControllerTests UserId = userId, OrganizationId = Guid.NewGuid(), Type = CipherType.Login, + ViewPassword = true, Data = @" { ""Uris"": [ diff --git a/util/Migrator/DbScripts/2025-02-04_00_CollectionPermissionEditExceptPWPerm.sql b/util/Migrator/DbScripts/2025-02-04_00_CollectionPermissionEditExceptPWPerm.sql new file mode 100644 index 0000000000..95013afaa4 --- /dev/null +++ b/util/Migrator/DbScripts/2025-02-04_00_CollectionPermissionEditExceptPWPerm.sql @@ -0,0 +1,118 @@ +CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_ReadByIdUserId] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + +SELECT + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [Attachments], + [CreationDate], + [RevisionDate], + [Favorite], + [FolderId], + [DeletedDate], + [Reprompt], + [Key], + [OrganizationUseTotp], + MAX ([Edit]) AS [Edit], + MAX ([ViewPassword]) AS [ViewPassword] +FROM + [dbo].[UserCipherDetails](@UserId) +WHERE + [Id] = @Id +GROUP BY + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [Attachments], + [CreationDate], + [RevisionDate], + [Favorite], + [FolderId], + [DeletedDate], + [Reprompt], + [Key], + [OrganizationUseTotp] +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[CollectionCipher_UpdateCollections] + @CipherId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @CollectionIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + DECLARE @OrgId UNIQUEIDENTIFIER = ( + SELECT TOP 1 + [OrganizationId] + FROM + [dbo].[Cipher] + WHERE + [Id] = @CipherId + ) + SELECT + C.[Id] + INTO #TempAvailableCollections + FROM + [dbo].[Collection] C + INNER JOIN + [Organization] O ON O.[Id] = C.[OrganizationId] + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[OrganizationId] = O.[Id] AND OU.[UserId] = @UserId + LEFT JOIN + [dbo].[CollectionUser] CU ON CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = OU.[Id] + LEFT JOIN + [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id] + LEFT JOIN + [dbo].[Group] G ON G.[Id] = GU.[GroupId] + LEFT JOIN + [dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId] + WHERE + O.[Id] = @OrgId + AND O.[Enabled] = 1 + AND OU.[Status] = 2 -- Confirmed + AND ( + CU.[ReadOnly] = 0 + OR CG.[ReadOnly] = 0 + ) + -- Insert new collection assignments + INSERT INTO [dbo].[CollectionCipher] ( + [CollectionId], + [CipherId] + ) + SELECT + [Id], + @CipherId + FROM @CollectionIds + WHERE [Id] IN (SELECT [Id] FROM [#TempAvailableCollections]) + AND NOT EXISTS ( + SELECT 1 + FROM [dbo].[CollectionCipher] + WHERE [CollectionId] = [@CollectionIds].[Id] + AND [CipherId] = @CipherId + ); + + -- Delete removed collection assignments + DELETE CC + FROM [dbo].[CollectionCipher] CC + WHERE CC.[CipherId] = @CipherId + AND CC.[CollectionId] IN (SELECT [Id] FROM [#TempAvailableCollections]) + AND CC.[CollectionId] NOT IN (SELECT [Id] FROM @CollectionIds); + + IF @OrgId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId + END + DROP TABLE #TempAvailableCollections; +END +GO From a8a08a0c8f21b9893b4a5831f61cfce1ad131d3c Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 5 Feb 2025 09:18:23 +0100 Subject: [PATCH 098/148] Remove the feature flag (#5331) --- .../Billing/Controllers/OrganizationSponsorshipsController.cs | 3 +-- src/Core/Constants.cs | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs index a7a4c39054..42263aa88b 100644 --- a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs @@ -1,6 +1,5 @@ using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response.Organizations; -using Bit.Core; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; using Bit.Core.AdminConsole.Repositories; @@ -107,7 +106,7 @@ public class OrganizationSponsorshipsController : Controller { var isFreeFamilyPolicyEnabled = false; var (isValid, sponsorship) = await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email); - if (isValid && _featureService.IsEnabled(FeatureFlagKeys.DisableFreeFamiliesSponsorship) && sponsorship.SponsoringOrganizationId.HasValue) + if (isValid && sponsorship.SponsoringOrganizationId.HasValue) { var policy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsorship.SponsoringOrganizationId.Value, PolicyType.FreeFamiliesSponsorshipPolicy); diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index f58f1f7157..cba146c959 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -155,7 +155,6 @@ public static class FeatureFlagKeys public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss"; public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss"; public const string SecurityTasks = "security-tasks"; - 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"; From 617bb5015fdde37a0e78d43a8086918409de41fb Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Wed, 5 Feb 2025 04:57:19 -0500 Subject: [PATCH 099/148] Removing the member access feature flag from the server (#5368) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index cba146c959..03d6a193a7 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -112,7 +112,6 @@ public static class FeatureFlagKeys /* Tools Team */ public const string ItemShare = "item-share"; public const string GeneratorToolsModernization = "generator-tools-modernization"; - public const string MemberAccessReport = "ac-2059-member-access-report"; public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications"; From 03c390de7405b6e2a2c0463d28ad406de6b66210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:47:06 +0000 Subject: [PATCH 100/148] =?UTF-8?q?[PM-15637]=20Notify=20Custom=20Users=20?= =?UTF-8?q?with=20=E2=80=9CManage=20Account=20Recovery=E2=80=9D=20permissi?= =?UTF-8?q?on=20for=20Device=20Approval=20Requests=20(#5359)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add stored procedure to read organization user details by role * Add OrganizationUserRepository method to retrieve OrganizationUser details by role * Enhance AuthRequestService to send notifications to custom users with ManageResetPassword permission * Enhance AuthRequestServiceTests to include custom user permissions and validate notification email recipients --- .../IOrganizationUserRepository.cs | 8 +++++ .../Implementations/AuthRequestService.cs | 30 +++++++++++++++-- .../OrganizationUserRepository.cs | 13 ++++++++ .../OrganizationUserRepository.cs | 21 ++++++++++++ ...OrganizationUser_ReadManyDetailsByRole.sql | 16 ++++++++++ .../Auth/Services/AuthRequestServiceTests.cs | 32 +++++++++++++++++-- ...-02-03_00_OrgUserReadManyDetailsByRole.sql | 16 ++++++++++ 7 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationUser_ReadManyDetailsByRole.sql create mode 100644 util/Migrator/DbScripts/2025-02-03_00_OrgUserReadManyDetailsByRole.sql diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs index 516b4614af..8825f9722a 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs @@ -60,4 +60,12 @@ public interface IOrganizationUserRepository : IRepository> GetManyByOrganizationWithClaimedDomainsAsync(Guid organizationId); Task RevokeManyByIdAsync(IEnumerable organizationUserIds); + + /// + /// Returns a list of OrganizationUsersUserDetails with the specified role. + /// + /// The organization to search within + /// The role to search for + /// A list of OrganizationUsersUserDetails with the specified role + Task> GetManyDetailsByRoleAsync(Guid organizationId, OrganizationUserType role); } diff --git a/src/Core/Auth/Services/Implementations/AuthRequestService.cs b/src/Core/Auth/Services/Implementations/AuthRequestService.cs index 5e41e3a679..b70a690338 100644 --- a/src/Core/Auth/Services/Implementations/AuthRequestService.cs +++ b/src/Core/Auth/Services/Implementations/AuthRequestService.cs @@ -297,10 +297,34 @@ public class AuthRequestService : IAuthRequestService return; } - var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync( + var adminEmails = await GetAdminAndAccountRecoveryEmailsAsync(organizationUser.OrganizationId); + + await _mailService.SendDeviceApprovalRequestedNotificationEmailAsync( + adminEmails, organizationUser.OrganizationId, + user.Email, + user.Name); + } + + /// + /// Returns a list of emails for admins and custom users with the ManageResetPassword permission. + /// + /// The organization to search within + private async Task> GetAdminAndAccountRecoveryEmailsAsync(Guid organizationId) + { + var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync( + organizationId, OrganizationUserType.Admin); - var adminEmails = admins.Select(a => a.Email).Distinct().ToList(); - await _mailService.SendDeviceApprovalRequestedNotificationEmailAsync(adminEmails, organizationUser.OrganizationId, user.Email, user.Name); + + var customUsers = await _organizationUserRepository.GetManyDetailsByRoleAsync( + organizationId, + OrganizationUserType.Custom); + + return admins.Select(a => a.Email) + .Concat(customUsers + .Where(a => a.GetPermissions().ManageResetPassword) + .Select(a => a.Email)) + .Distinct() + .ToList(); } } diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs index 42f79852f3..9b77fb216e 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -567,4 +567,17 @@ public class OrganizationUserRepository : Repository, IO new { OrganizationUserIds = JsonSerializer.Serialize(organizationUserIds), Status = OrganizationUserStatusType.Revoked }, commandType: CommandType.StoredProcedure); } + + public async Task> GetManyDetailsByRoleAsync(Guid organizationId, OrganizationUserType role) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[OrganizationUser_ReadManyDetailsByRole]", + new { OrganizationId = organizationId, Role = role }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index 007ff1a7ff..ef6460df0e 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -733,4 +733,25 @@ public class OrganizationUserRepository : Repository> GetManyDetailsByRoleAsync(Guid organizationId, OrganizationUserType role) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var query = from ou in dbContext.OrganizationUsers + join u in dbContext.Users + on ou.UserId equals u.Id + where ou.OrganizationId == organizationId && + ou.Type == role && + ou.Status == OrganizationUserStatusType.Confirmed + select new OrganizationUserUserDetails + { + Id = ou.Id, + Email = ou.Email ?? u.Email, + Permissions = ou.Permissions + }; + return await query.ToListAsync(); + } + } } diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadManyDetailsByRole.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadManyDetailsByRole.sql new file mode 100644 index 0000000000..e8bf8bb701 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadManyDetailsByRole.sql @@ -0,0 +1,16 @@ +CREATE PROCEDURE [dbo].[OrganizationUser_ReadManyDetailsByRole] + @OrganizationId UNIQUEIDENTIFIER, + @Role TINYINT +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationUserUserDetailsView] + WHERE + OrganizationId = @OrganizationId + AND Status = 2 -- 2 = Confirmed + AND [Type] = @Role +END diff --git a/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs b/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs index 3894ac90a8..8feef2facc 100644 --- a/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs +++ b/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs @@ -7,11 +7,13 @@ using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; +using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; @@ -347,14 +349,24 @@ public class AuthRequestServiceTests User user, OrganizationUser organizationUser1, OrganizationUserUserDetails admin1, + OrganizationUserUserDetails customUser1, OrganizationUser organizationUser2, OrganizationUserUserDetails admin2, - OrganizationUserUserDetails admin3) + OrganizationUserUserDetails admin3, + OrganizationUserUserDetails customUser2) { createModel.Type = AuthRequestType.AdminApproval; user.Email = createModel.Email; organizationUser1.UserId = user.Id; organizationUser2.UserId = user.Id; + customUser1.Permissions = CoreHelpers.ClassToJsonData(new Permissions + { + ManageResetPassword = false, + }); + customUser2.Permissions = CoreHelpers.ClassToJsonData(new Permissions + { + ManageResetPassword = true, + }); sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.DeviceApprovalRequestAdminNotifications) @@ -392,6 +404,13 @@ public class AuthRequestServiceTests admin1, ]); + sutProvider.GetDependency() + .GetManyDetailsByRoleAsync(organizationUser1.OrganizationId, OrganizationUserType.Custom) + .Returns( + [ + customUser1, + ]); + sutProvider.GetDependency() .GetManyByMinimumRoleAsync(organizationUser2.OrganizationId, OrganizationUserType.Admin) .Returns( @@ -400,6 +419,13 @@ public class AuthRequestServiceTests admin3, ]); + sutProvider.GetDependency() + .GetManyDetailsByRoleAsync(organizationUser2.OrganizationId, OrganizationUserType.Custom) + .Returns( + [ + customUser2, + ]); + sutProvider.GetDependency() .CreateAsync(Arg.Any()) .Returns(c => c.ArgAt(0)); @@ -435,7 +461,9 @@ public class AuthRequestServiceTests await sutProvider.GetDependency() .Received(1) .SendDeviceApprovalRequestedNotificationEmailAsync( - Arg.Is>(emails => emails.Count() == 2 && emails.Contains(admin2.Email) && emails.Contains(admin3.Email)), + Arg.Is>(emails => emails.Count() == 3 && + emails.Contains(admin2.Email) && emails.Contains(admin3.Email) && + emails.Contains(customUser2.Email)), organizationUser2.OrganizationId, user.Email, user.Name); diff --git a/util/Migrator/DbScripts/2025-02-03_00_OrgUserReadManyDetailsByRole.sql b/util/Migrator/DbScripts/2025-02-03_00_OrgUserReadManyDetailsByRole.sql new file mode 100644 index 0000000000..4d687f0bb1 --- /dev/null +++ b/util/Migrator/DbScripts/2025-02-03_00_OrgUserReadManyDetailsByRole.sql @@ -0,0 +1,16 @@ +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_ReadManyDetailsByRole] + @OrganizationId UNIQUEIDENTIFIER, + @Role TINYINT +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationUserUserDetailsView] + WHERE + OrganizationId = @OrganizationId + AND Status = 2 -- 2 = Confirmed + AND [Type] = @Role +END From 77364549fa9dd756a02c0bea04761b7f01beaa76 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Wed, 5 Feb 2025 10:03:13 -0500 Subject: [PATCH 101/148] [PM-16157] Add feature flag for mTLS support in Android client (#5335) Add a feature flag to control support for selecting a mutual TLS client certificate within the Android client. --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 03d6a193a7..ba3b8b0795 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -173,6 +173,7 @@ public static class FeatureFlagKeys public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication"; public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync"; public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal"; + public const string AndroidMutualTls = "mutual-tls"; public static List GetAllKeys() { From a971a18719b4a9a8cf3af77270b1799ed6942083 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:32:27 -0500 Subject: [PATCH 102/148] [PM-17957] Pin Transitive Deps (#5371) * Remove duplicate quartz reference * Pin Core packages * Pin Notifications packages --- src/Core/Core.csproj | 6 +++++- src/Notifications/Notifications.csproj | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 7b319e56c9..a2d4660194 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -51,7 +51,6 @@ - @@ -73,6 +72,11 @@ + + + + + diff --git a/src/Notifications/Notifications.csproj b/src/Notifications/Notifications.csproj index 68ae96963e..4d19f7faf9 100644 --- a/src/Notifications/Notifications.csproj +++ b/src/Notifications/Notifications.csproj @@ -11,6 +11,10 @@ + + + + From 46004b9c6849f0ca7c8ddbfce930cbb2f72a9c0a Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Wed, 5 Feb 2025 16:56:01 -0500 Subject: [PATCH 103/148] [PM-14381] Add POST /tasks/bulk-create endpoint (#5188) * [PM-14378] Introduce GetCipherPermissionsForOrganization query for Dapper CipherRepository * [PM-14378] Introduce GetCipherPermissionsForOrganization method for Entity Framework * [PM-14378] Add integration tests for new repository method * [PM-14378] Introduce IGetCipherPermissionsForUserQuery CQRS query * [PM-14378] Introduce SecurityTaskOperationRequirement * [PM-14378] Introduce SecurityTaskAuthorizationHandler.cs * [PM-14378] Introduce SecurityTaskOrganizationAuthorizationHandler.cs * [PM-14378] Register new authorization handlers * [PM-14378] Formatting * [PM-14378] Add unit tests for GetCipherPermissionsForUserQuery * [PM-15378] Cleanup SecurityTaskAuthorizationHandler and add tests * [PM-14378] Add tests for SecurityTaskOrganizationAuthorizationHandler * [PM-14378] Formatting * [PM-14378] Update date in migration file * [PM-14378] Add missing awaits * Added bulk create request model * Created sproc to create bulk security tasks * Renamed tasks to SecurityTasksInput * Added create many implementation for sqlserver and ef core * removed trailing comma * created ef implementatin for create many and added integration test * Refactored request model * Refactored request model * created create many tasks command interface and class * added security authorization handler work temp * Added the implementation for the create manys tasks command * Added comment * Changed return to return list of created security tasks * Registered command * Completed bulk create action * Added unit tests for the command * removed hard coded table name * Fixed lint issue * Added JsonConverter attribute to allow enum value to be passed as string * Removed makshift security task operations * Fixed references * Removed old migration * Rebased * [PM-14378] Introduce GetCipherPermissionsForOrganization query for Dapper CipherRepository * [PM-14378] Introduce GetCipherPermissionsForOrganization method for Entity Framework * [PM-14378] Add unit tests for GetCipherPermissionsForUserQuery * Completed bulk create action * bumped migration version * Fixed lint issue * Removed complex sql data type in favour of json string * Register IGetTasksForOrganizationQuery * Fixed lint issue * Removed tasks grouping * Fixed linting * Removed unused code * Removed unused code * Aligned with client change * Fixed linting --------- Co-authored-by: Shane Melton --- .../Controllers/SecurityTaskController.cs | 21 ++++- .../BulkCreateSecurityTasksRequestModel.cs | 8 ++ .../Authorization/SecurityTaskOperations.cs | 16 ---- .../Vault/Commands/CreateManyTasksCommand.cs | 65 ++++++++++++++ .../Interfaces/ICreateManyTasksCommand.cs | 17 ++++ .../Commands/MarkTaskAsCompletedCommand.cs | 2 +- .../Models/Api/SecurityTaskCreateRequest.cs | 9 ++ .../Repositories/ISecurityTaskRepository.cs | 7 ++ .../Vault/VaultServiceCollectionExtensions.cs | 1 + src/Infrastructure.Dapper/DapperHelpers.cs | 2 +- .../Repositories/SecurityTaskRepository.cs | 26 ++++++ .../Repositories/SecurityTaskRepository.cs | 24 ++++++ .../SecurityTask/SecurityTask_CreateMany.sql | 55 ++++++++++++ .../Commands/CreateManyTasksCommandTest.cs | 85 +++++++++++++++++++ .../MarkTaskAsCompletedCommandTest.cs | 2 +- .../SecurityTaskRepositoryTests.cs | 43 ++++++++++ .../2025-01-22_00_SecurityTaskCreateMany.sql | 55 ++++++++++++ 17 files changed, 418 insertions(+), 20 deletions(-) create mode 100644 src/Api/Vault/Models/Request/BulkCreateSecurityTasksRequestModel.cs delete mode 100644 src/Core/Vault/Authorization/SecurityTaskOperations.cs create mode 100644 src/Core/Vault/Commands/CreateManyTasksCommand.cs create mode 100644 src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs create mode 100644 src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs create mode 100644 src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_CreateMany.sql create mode 100644 test/Core.Test/Vault/Commands/CreateManyTasksCommandTest.cs create mode 100644 util/Migrator/DbScripts/2025-01-22_00_SecurityTaskCreateMany.sql diff --git a/src/Api/Vault/Controllers/SecurityTaskController.cs b/src/Api/Vault/Controllers/SecurityTaskController.cs index 14ef0e5e4e..88b7aed9c6 100644 --- a/src/Api/Vault/Controllers/SecurityTaskController.cs +++ b/src/Api/Vault/Controllers/SecurityTaskController.cs @@ -1,4 +1,5 @@ using Bit.Api.Models.Response; +using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Response; using Bit.Core; using Bit.Core.Services; @@ -20,17 +21,20 @@ public class SecurityTaskController : Controller private readonly IGetTaskDetailsForUserQuery _getTaskDetailsForUserQuery; private readonly IMarkTaskAsCompleteCommand _markTaskAsCompleteCommand; private readonly IGetTasksForOrganizationQuery _getTasksForOrganizationQuery; + private readonly ICreateManyTasksCommand _createManyTasksCommand; public SecurityTaskController( IUserService userService, IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery, IMarkTaskAsCompleteCommand markTaskAsCompleteCommand, - IGetTasksForOrganizationQuery getTasksForOrganizationQuery) + IGetTasksForOrganizationQuery getTasksForOrganizationQuery, + ICreateManyTasksCommand createManyTasksCommand) { _userService = userService; _getTaskDetailsForUserQuery = getTaskDetailsForUserQuery; _markTaskAsCompleteCommand = markTaskAsCompleteCommand; _getTasksForOrganizationQuery = getTasksForOrganizationQuery; + _createManyTasksCommand = createManyTasksCommand; } /// @@ -71,4 +75,19 @@ public class SecurityTaskController : Controller var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList(); return new ListResponseModel(response); } + + /// + /// Bulk create security tasks for an organization. + /// + /// + /// + /// A list response model containing the security tasks created for the organization. + [HttpPost("{orgId:guid}/bulk-create")] + public async Task> BulkCreateTasks(Guid orgId, + [FromBody] BulkCreateSecurityTasksRequestModel model) + { + var securityTasks = await _createManyTasksCommand.CreateAsync(orgId, model.Tasks); + var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList(); + return new ListResponseModel(response); + } } diff --git a/src/Api/Vault/Models/Request/BulkCreateSecurityTasksRequestModel.cs b/src/Api/Vault/Models/Request/BulkCreateSecurityTasksRequestModel.cs new file mode 100644 index 0000000000..6c8c7e03b3 --- /dev/null +++ b/src/Api/Vault/Models/Request/BulkCreateSecurityTasksRequestModel.cs @@ -0,0 +1,8 @@ +using Bit.Core.Vault.Models.Api; + +namespace Bit.Api.Vault.Models.Request; + +public class BulkCreateSecurityTasksRequestModel +{ + public IEnumerable Tasks { get; set; } +} diff --git a/src/Core/Vault/Authorization/SecurityTaskOperations.cs b/src/Core/Vault/Authorization/SecurityTaskOperations.cs deleted file mode 100644 index 77b504723f..0000000000 --- a/src/Core/Vault/Authorization/SecurityTaskOperations.cs +++ /dev/null @@ -1,16 +0,0 @@ -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/CreateManyTasksCommand.cs b/src/Core/Vault/Commands/CreateManyTasksCommand.cs new file mode 100644 index 0000000000..1b21f202eb --- /dev/null +++ b/src/Core/Vault/Commands/CreateManyTasksCommand.cs @@ -0,0 +1,65 @@ +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Utilities; +using Bit.Core.Vault.Authorization.SecurityTasks; +using Bit.Core.Vault.Commands.Interfaces; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; +using Bit.Core.Vault.Models.Api; +using Bit.Core.Vault.Repositories; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.Vault.Commands; + +public class CreateManyTasksCommand : ICreateManyTasksCommand +{ + private readonly IAuthorizationService _authorizationService; + private readonly ICurrentContext _currentContext; + private readonly ISecurityTaskRepository _securityTaskRepository; + + public CreateManyTasksCommand( + ISecurityTaskRepository securityTaskRepository, + IAuthorizationService authorizationService, + ICurrentContext currentContext) + { + _securityTaskRepository = securityTaskRepository; + _authorizationService = authorizationService; + _currentContext = currentContext; + } + + /// + public async Task> CreateAsync(Guid organizationId, + IEnumerable tasks) + { + if (!_currentContext.UserId.HasValue) + { + throw new NotFoundException(); + } + + var tasksList = tasks?.ToList(); + + if (tasksList is null || tasksList.Count == 0) + { + throw new BadRequestException("No tasks provided."); + } + + var securityTasks = tasksList.Select(t => new SecurityTask + { + OrganizationId = organizationId, + CipherId = t.CipherId, + Type = t.Type, + Status = SecurityTaskStatus.Pending + }).ToList(); + + // Verify authorization for each task + foreach (var task in securityTasks) + { + await _authorizationService.AuthorizeOrThrowAsync( + _currentContext.HttpContext.User, + task, + SecurityTaskOperations.Create); + } + + return await _securityTaskRepository.CreateManyAsync(securityTasks); + } +} diff --git a/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs b/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs new file mode 100644 index 0000000000..3aa0f85070 --- /dev/null +++ b/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs @@ -0,0 +1,17 @@ +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Models.Api; + +namespace Bit.Core.Vault.Commands.Interfaces; + +public interface ICreateManyTasksCommand +{ + /// + /// Creates multiple security tasks for an organization. + /// Each task must be authorized and the user must have the Create permission + /// and associated ciphers must belong to the organization. + /// + /// The + /// + /// Collection of created security tasks + Task> CreateAsync(Guid organizationId, IEnumerable tasks); +} diff --git a/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs b/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs index b46fb0cecb..77b8a8625c 100644 --- a/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs +++ b/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs @@ -1,7 +1,7 @@ using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Utilities; -using Bit.Core.Vault.Authorization; +using Bit.Core.Vault.Authorization.SecurityTasks; using Bit.Core.Vault.Commands.Interfaces; using Bit.Core.Vault.Enums; using Bit.Core.Vault.Repositories; diff --git a/src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs b/src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs new file mode 100644 index 0000000000..f865871380 --- /dev/null +++ b/src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs @@ -0,0 +1,9 @@ +using Bit.Core.Vault.Enums; + +namespace Bit.Core.Vault.Models.Api; + +public class SecurityTaskCreateRequest +{ + public SecurityTaskType Type { get; set; } + public Guid? CipherId { get; set; } +} diff --git a/src/Core/Vault/Repositories/ISecurityTaskRepository.cs b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs index c236172533..cc8303345d 100644 --- a/src/Core/Vault/Repositories/ISecurityTaskRepository.cs +++ b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs @@ -21,4 +21,11 @@ public interface ISecurityTaskRepository : IRepository /// Optional filter for task status. If not provided, returns tasks of all statuses /// Task> GetManyByOrganizationIdStatusAsync(Guid organizationId, SecurityTaskStatus? status = null); + + /// + /// Creates bulk security tasks for an organization. + /// + /// Collection of tasks to create + /// Collection of created security tasks + Task> CreateManyAsync(IEnumerable tasks); } diff --git a/src/Core/Vault/VaultServiceCollectionExtensions.cs b/src/Core/Vault/VaultServiceCollectionExtensions.cs index 169a62d12d..fcb9259135 100644 --- a/src/Core/Vault/VaultServiceCollectionExtensions.cs +++ b/src/Core/Vault/VaultServiceCollectionExtensions.cs @@ -21,5 +21,6 @@ public static class VaultServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Infrastructure.Dapper/DapperHelpers.cs b/src/Infrastructure.Dapper/DapperHelpers.cs index c256612447..9a67af3a93 100644 --- a/src/Infrastructure.Dapper/DapperHelpers.cs +++ b/src/Infrastructure.Dapper/DapperHelpers.cs @@ -81,7 +81,7 @@ public class DataTableBuilder return true; } - // Value type properties will implicitly box into the object so + // Value type properties will implicitly box into the object so // we need to look past the Convert expression // i => (System.Object?)i.Id if ( diff --git a/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs index 35dace9a9e..f7a5f3b878 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs @@ -1,4 +1,5 @@ using System.Data; +using System.Text.Json; using Bit.Core.Settings; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Enums; @@ -46,4 +47,29 @@ public class SecurityTaskRepository : Repository, ISecurityT return results.ToList(); } + + /// + public async Task> CreateManyAsync(IEnumerable tasks) + { + var tasksList = tasks?.ToList(); + if (tasksList is null || tasksList.Count == 0) + { + return Array.Empty(); + } + + foreach (var task in tasksList) + { + task.SetNewId(); + } + + var tasksJson = JsonSerializer.Serialize(tasksList); + + await using var connection = new SqlConnection(ConnectionString); + await connection.ExecuteAsync( + $"[{Schema}].[{Table}_CreateMany]", + new { SecurityTasksJson = tasksJson }, + commandType: CommandType.StoredProcedure); + + return tasksList; + } } diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs index 5adfdc4c76..a3ba2632fe 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs @@ -52,4 +52,28 @@ public class SecurityTaskRepository : Repository st.CreationDate).ToListAsync(); } + + /// + public async Task> CreateManyAsync( + IEnumerable tasks) + { + var tasksList = tasks?.ToList(); + if (tasksList is null || tasksList.Count == 0) + { + return Array.Empty(); + } + + foreach (var task in tasksList) + { + task.SetNewId(); + } + + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var entities = Mapper.Map>(tasksList); + await dbContext.AddRangeAsync(entities); + await dbContext.SaveChangesAsync(); + + return tasksList; + } } diff --git a/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_CreateMany.sql b/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_CreateMany.sql new file mode 100644 index 0000000000..9e60f2ad1b --- /dev/null +++ b/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_CreateMany.sql @@ -0,0 +1,55 @@ +CREATE PROCEDURE [dbo].[SecurityTask_CreateMany] + @SecurityTasksJson NVARCHAR(MAX) +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #TempSecurityTasks + ( + [Id] UNIQUEIDENTIFIER, + [OrganizationId] UNIQUEIDENTIFIER, + [CipherId] UNIQUEIDENTIFIER, + [Type] TINYINT, + [Status] TINYINT, + [CreationDate] DATETIME2(7), + [RevisionDate] DATETIME2(7) + ) + + INSERT INTO #TempSecurityTasks + ([Id], + [OrganizationId], + [CipherId], + [Type], + [Status], + [CreationDate], + [RevisionDate]) + SELECT CAST(JSON_VALUE([value], '$.Id') AS UNIQUEIDENTIFIER), + CAST(JSON_VALUE([value], '$.OrganizationId') AS UNIQUEIDENTIFIER), + CAST(JSON_VALUE([value], '$.CipherId') AS UNIQUEIDENTIFIER), + CAST(JSON_VALUE([value], '$.Type') AS TINYINT), + CAST(JSON_VALUE([value], '$.Status') AS TINYINT), + CAST(JSON_VALUE([value], '$.CreationDate') AS DATETIME2(7)), + CAST(JSON_VALUE([value], '$.RevisionDate') AS DATETIME2(7)) + FROM OPENJSON(@SecurityTasksJson) ST + + INSERT INTO [dbo].[SecurityTask] + ( + [Id], + [OrganizationId], + [CipherId], + [Type], + [Status], + [CreationDate], + [RevisionDate] + ) + SELECT [Id], + [OrganizationId], + [CipherId], + [Type], + [Status], + [CreationDate], + [RevisionDate] + FROM #TempSecurityTasks + + DROP TABLE #TempSecurityTasks +END diff --git a/test/Core.Test/Vault/Commands/CreateManyTasksCommandTest.cs b/test/Core.Test/Vault/Commands/CreateManyTasksCommandTest.cs new file mode 100644 index 0000000000..23e92965f2 --- /dev/null +++ b/test/Core.Test/Vault/Commands/CreateManyTasksCommandTest.cs @@ -0,0 +1,85 @@ +using System.Security.Claims; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Test.Vault.AutoFixture; +using Bit.Core.Vault.Authorization.SecurityTasks; +using Bit.Core.Vault.Commands; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Models.Api; +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 CreateManyTasksCommandTest +{ + private static void Setup(SutProvider sutProvider, Guid? userId, + bool authorizedCreate = false) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(SecurityTaskOperations.Create))) + .Returns(authorizedCreate ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_NotLoggedIn_NotFoundException( + SutProvider sutProvider, + Guid organizationId, + IEnumerable tasks) + { + Setup(sutProvider, null, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(organizationId, tasks)); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_NoTasksProvided_BadRequestException( + SutProvider sutProvider, + Guid organizationId) + { + Setup(sutProvider, Guid.NewGuid()); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(organizationId, null)); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_AuthorizationFailed_NotFoundException( + SutProvider sutProvider, + Guid organizationId, + IEnumerable tasks) + { + Setup(sutProvider, Guid.NewGuid()); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(organizationId, tasks)); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_AuthorizationSucceeded_ReturnsSecurityTasks( + SutProvider sutProvider, + Guid organizationId, + IEnumerable tasks, + ICollection securityTasks) + { + Setup(sutProvider, Guid.NewGuid(), true); + sutProvider.GetDependency() + .CreateManyAsync(Arg.Any>()) + .Returns(securityTasks); + + var result = await sutProvider.Sut.CreateAsync(organizationId, tasks); + + Assert.Equal(securityTasks, result); + } +} diff --git a/test/Core.Test/Vault/Commands/MarkTaskAsCompletedCommandTest.cs b/test/Core.Test/Vault/Commands/MarkTaskAsCompletedCommandTest.cs index 82550df48d..ca9a42cdb3 100644 --- a/test/Core.Test/Vault/Commands/MarkTaskAsCompletedCommandTest.cs +++ b/test/Core.Test/Vault/Commands/MarkTaskAsCompletedCommandTest.cs @@ -3,7 +3,7 @@ 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.Authorization.SecurityTasks; using Bit.Core.Vault.Commands; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Repositories; diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs index 2010c90a5e..eb5a310db3 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs @@ -223,4 +223,47 @@ public class SecurityTaskRepositoryTests Assert.DoesNotContain(task1, completedTasks, new SecurityTaskComparer()); Assert.DoesNotContain(task3, completedTasks, new SecurityTaskComparer()); } + + [DatabaseTheory, DatabaseData] + public async Task CreateManyAsync( + IOrganizationRepository organizationRepository, + ICipherRepository cipherRepository, + ISecurityTaskRepository securityTaskRepository) + { + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + PlanType = PlanType.EnterpriseAnnually, + Plan = "Test Plan", + BillingEmail = "" + }); + + var cipher1 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", }; + await cipherRepository.CreateAsync(cipher1); + + var cipher2 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", }; + await cipherRepository.CreateAsync(cipher2); + + var tasks = new List + { + new() + { + OrganizationId = organization.Id, + CipherId = cipher1.Id, + Status = SecurityTaskStatus.Pending, + Type = SecurityTaskType.UpdateAtRiskCredential, + }, + new() + { + OrganizationId = organization.Id, + CipherId = cipher2.Id, + Status = SecurityTaskStatus.Completed, + Type = SecurityTaskType.UpdateAtRiskCredential, + } + }; + + var taskIds = await securityTaskRepository.CreateManyAsync(tasks); + + Assert.Equal(2, taskIds.Count); + } } diff --git a/util/Migrator/DbScripts/2025-01-22_00_SecurityTaskCreateMany.sql b/util/Migrator/DbScripts/2025-01-22_00_SecurityTaskCreateMany.sql new file mode 100644 index 0000000000..6bf797eccd --- /dev/null +++ b/util/Migrator/DbScripts/2025-01-22_00_SecurityTaskCreateMany.sql @@ -0,0 +1,55 @@ +-- SecurityTask_CreateMany +CREATE OR ALTER PROCEDURE [dbo].[SecurityTask_CreateMany] + @SecurityTasksJson NVARCHAR(MAX) +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #TempSecurityTasks + ( + [Id] UNIQUEIDENTIFIER, + [OrganizationId] UNIQUEIDENTIFIER, + [CipherId] UNIQUEIDENTIFIER, + [Type] TINYINT, + [Status] TINYINT, + [CreationDate] DATETIME2(7), + [RevisionDate] DATETIME2(7) + ) + + INSERT INTO #TempSecurityTasks + ([Id], + [OrganizationId], + [CipherId], + [Type], + [Status], + [CreationDate], + [RevisionDate]) + SELECT CAST(JSON_VALUE([value], '$.Id') AS UNIQUEIDENTIFIER), + CAST(JSON_VALUE([value], '$.OrganizationId') AS UNIQUEIDENTIFIER), + CAST(JSON_VALUE([value], '$.CipherId') AS UNIQUEIDENTIFIER), + CAST(JSON_VALUE([value], '$.Type') AS TINYINT), + CAST(JSON_VALUE([value], '$.Status') AS TINYINT), + CAST(JSON_VALUE([value], '$.CreationDate') AS DATETIME2(7)), + CAST(JSON_VALUE([value], '$.RevisionDate') AS DATETIME2(7)) + FROM OPENJSON(@SecurityTasksJson) ST + + INSERT INTO [dbo].[SecurityTask] + ([Id], + [OrganizationId], + [CipherId], + [Type], + [Status], + [CreationDate], + [RevisionDate]) + SELECT [Id], + [OrganizationId], + [CipherId], + [Type], + [Status], + [CreationDate], + [RevisionDate] + FROM #TempSecurityTasks + + DROP TABLE #TempSecurityTasks +END +GO From daf2696a813fba07a704e9b552b5e5151f64bd4f Mon Sep 17 00:00:00 2001 From: Graham Walker Date: Wed, 5 Feb 2025 16:36:18 -0600 Subject: [PATCH 104/148] PM-16085 - Increase import limitations (#5275) * PM-16261 move ImportCiphersAsync to the tools team and create services using CQRS design pattern * PM-16261 fix renaming methods and add unit tests for succes and bad request exception * PM-16261 clean up old code from test * make import limits configurable via appsettings * PM-16085 fix issue with appSettings converting to globalSettings for new cipher import limits --- src/Api/Tools/Controllers/ImportCiphersController.cs | 5 +++-- src/Api/appsettings.json | 5 +++++ src/Core/Settings/GlobalSettings.cs | 8 ++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Api/Tools/Controllers/ImportCiphersController.cs b/src/Api/Tools/Controllers/ImportCiphersController.cs index 4f4e76f6e3..c268500f71 100644 --- a/src/Api/Tools/Controllers/ImportCiphersController.cs +++ b/src/Api/Tools/Controllers/ImportCiphersController.cs @@ -64,8 +64,9 @@ public class ImportCiphersController : Controller [FromBody] ImportOrganizationCiphersRequestModel model) { if (!_globalSettings.SelfHosted && - (model.Ciphers.Count() > 7000 || model.CollectionRelationships.Count() > 14000 || - model.Collections.Count() > 2000)) + (model.Ciphers.Count() > _globalSettings.ImportCiphersLimitation.CiphersLimit || + model.CollectionRelationships.Count() > _globalSettings.ImportCiphersLimitation.CollectionRelationshipsLimit || + model.Collections.Count() > _globalSettings.ImportCiphersLimitation.CollectionsLimit)) { throw new BadRequestException("You cannot import this much data at once."); } diff --git a/src/Api/appsettings.json b/src/Api/appsettings.json index c04539a9fe..98b210cb1e 100644 --- a/src/Api/appsettings.json +++ b/src/Api/appsettings.json @@ -56,6 +56,11 @@ "publicKey": "SECRET", "privateKey": "SECRET" }, + "importCiphersLimitation": { + "ciphersLimit": 40000, + "collectionRelationshipsLimit": 80000, + "collectionsLimit": 2000 + }, "bitPay": { "production": false, "token": "SECRET", diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index d039102eb9..a63a36c1c0 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -70,6 +70,7 @@ public class GlobalSettings : IGlobalSettings public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings(); public virtual DuoSettings Duo { get; set; } = new DuoSettings(); public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings(); + public virtual ImportCiphersLimitationSettings ImportCiphersLimitation { get; set; } = new ImportCiphersLimitationSettings(); public virtual BitPaySettings BitPay { get; set; } = new BitPaySettings(); public virtual AmazonSettings Amazon { get; set; } = new AmazonSettings(); public virtual ServiceBusSettings ServiceBus { get; set; } = new ServiceBusSettings(); @@ -521,6 +522,13 @@ public class GlobalSettings : IGlobalSettings public string PrivateKey { get; set; } } + public class ImportCiphersLimitationSettings + { + public int CiphersLimit { get; set; } + public int CollectionRelationshipsLimit { get; set; } + public int CollectionsLimit { get; set; } + } + public class BitPaySettings { public bool Production { get; set; } From 1c3ea1151c69aece541810819b8c1b6a31452f9b Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 6 Feb 2025 09:22:16 +0100 Subject: [PATCH 105/148] [PM-16482]NullReferenceException in CustomerUpdatedHandler due to uninitialized dependency (#5349) * Changes to throw exact errors * Add some logging to each error state Signed-off-by: Cy Okeke --------- Signed-off-by: Cy Okeke --- .../Implementations/CustomerUpdatedHandler.cs | 62 +++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/src/Billing/Services/Implementations/CustomerUpdatedHandler.cs b/src/Billing/Services/Implementations/CustomerUpdatedHandler.cs index ec70697c01..6deb0bc330 100644 --- a/src/Billing/Services/Implementations/CustomerUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/CustomerUpdatedHandler.cs @@ -14,19 +14,22 @@ public class CustomerUpdatedHandler : ICustomerUpdatedHandler private readonly ICurrentContext _currentContext; private readonly IStripeEventService _stripeEventService; private readonly IStripeEventUtilityService _stripeEventUtilityService; + private readonly ILogger _logger; public CustomerUpdatedHandler( IOrganizationRepository organizationRepository, IReferenceEventService referenceEventService, ICurrentContext currentContext, IStripeEventService stripeEventService, - IStripeEventUtilityService stripeEventUtilityService) + IStripeEventUtilityService stripeEventUtilityService, + ILogger logger) { - _organizationRepository = organizationRepository; + _organizationRepository = organizationRepository ?? throw new ArgumentNullException(nameof(organizationRepository)); _referenceEventService = referenceEventService; _currentContext = currentContext; _stripeEventService = stripeEventService; _stripeEventUtilityService = stripeEventUtilityService; + _logger = logger; } /// @@ -35,25 +38,76 @@ public class CustomerUpdatedHandler : ICustomerUpdatedHandler /// public async Task HandleAsync(Event parsedEvent) { - var customer = await _stripeEventService.GetCustomer(parsedEvent, true, ["subscriptions"]); - if (customer.Subscriptions == null || !customer.Subscriptions.Any()) + if (parsedEvent == null) { + _logger.LogError("Parsed event was null in CustomerUpdatedHandler"); + throw new ArgumentNullException(nameof(parsedEvent)); + } + + if (_stripeEventService == null) + { + _logger.LogError("StripeEventService was not initialized in CustomerUpdatedHandler"); + throw new InvalidOperationException($"{nameof(_stripeEventService)} is not initialized"); + } + + var customer = await _stripeEventService.GetCustomer(parsedEvent, true, ["subscriptions"]); + if (customer?.Subscriptions == null || !customer.Subscriptions.Any()) + { + _logger.LogWarning("Customer or subscriptions were null or empty in CustomerUpdatedHandler. Customer ID: {CustomerId}", customer?.Id); return; } var subscription = customer.Subscriptions.First(); + if (subscription.Metadata == null) + { + _logger.LogWarning("Subscription metadata was null in CustomerUpdatedHandler. Subscription ID: {SubscriptionId}", subscription.Id); + return; + } + + if (_stripeEventUtilityService == null) + { + _logger.LogError("StripeEventUtilityService was not initialized in CustomerUpdatedHandler"); + throw new InvalidOperationException($"{nameof(_stripeEventUtilityService)} is not initialized"); + } + var (organizationId, _, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata); if (!organizationId.HasValue) { + _logger.LogWarning("Organization ID was not found in subscription metadata. Subscription ID: {SubscriptionId}", subscription.Id); return; } + if (_organizationRepository == null) + { + _logger.LogError("OrganizationRepository was not initialized in CustomerUpdatedHandler"); + throw new InvalidOperationException($"{nameof(_organizationRepository)} is not initialized"); + } + var organization = await _organizationRepository.GetByIdAsync(organizationId.Value); + + if (organization == null) + { + _logger.LogWarning("Organization not found. Organization ID: {OrganizationId}", organizationId.Value); + return; + } + organization.BillingEmail = customer.Email; await _organizationRepository.ReplaceAsync(organization); + if (_referenceEventService == null) + { + _logger.LogError("ReferenceEventService was not initialized in CustomerUpdatedHandler"); + throw new InvalidOperationException($"{nameof(_referenceEventService)} is not initialized"); + } + + if (_currentContext == null) + { + _logger.LogError("CurrentContext was not initialized in CustomerUpdatedHandler"); + throw new InvalidOperationException($"{nameof(_currentContext)} is not initialized"); + } + await _referenceEventService.RaiseEventAsync( new ReferenceEvent(ReferenceEventType.OrganizationEditedInStripe, organization, _currentContext)); } From a12b61cc9ea48cf8132f2427747f6c4b11f0eb58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Thu, 6 Feb 2025 10:28:12 +0000 Subject: [PATCH 106/148] [PM-17168] Sync organization user revoked/restored status immediately via push notification (#5330) * [PM-17168] Add push notification for revoked and restored organization users * Add feature flag for push notification on user revoke/restore actions * Add tests for user revocation and restoration with push sync feature flag enabled --- .../Implementations/OrganizationService.cs | 28 ++ src/Core/Constants.cs | 1 + .../Services/OrganizationServiceTests.cs | 267 +++++++++++++----- 3 files changed, 230 insertions(+), 66 deletions(-) diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 70a3227a71..6194aea6c7 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -1951,6 +1951,11 @@ public class OrganizationService : IOrganizationService await RepositoryRevokeUserAsync(organizationUser); await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked); + + if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue) + { + await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value); + } } public async Task RevokeUserAsync(OrganizationUser organizationUser, @@ -1958,6 +1963,11 @@ public class OrganizationService : IOrganizationService { await RepositoryRevokeUserAsync(organizationUser); await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, systemUser); + + if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue) + { + await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value); + } } private async Task RepositoryRevokeUserAsync(OrganizationUser organizationUser) @@ -2023,6 +2033,10 @@ public class OrganizationService : IOrganizationService await _organizationUserRepository.RevokeAsync(organizationUser.Id); organizationUser.Status = OrganizationUserStatusType.Revoked; await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked); + if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue) + { + await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value); + } result.Add(Tuple.Create(organizationUser, "")); } @@ -2050,12 +2064,22 @@ public class OrganizationService : IOrganizationService await RepositoryRestoreUserAsync(organizationUser); await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored); + + if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue) + { + await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value); + } } public async Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser) { await RepositoryRestoreUserAsync(organizationUser); await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, systemUser); + + if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue) + { + await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value); + } } private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser) @@ -2147,6 +2171,10 @@ public class OrganizationService : IOrganizationService await _organizationUserRepository.RestoreAsync(organizationUser.Id, status); organizationUser.Status = status; await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored); + if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue) + { + await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value); + } result.Add(Tuple.Create(organizationUser, "")); } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index ba3b8b0795..3e59a9390b 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -108,6 +108,7 @@ public static class FeatureFlagKeys public const string IntegrationPage = "pm-14505-admin-console-integration-page"; public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications"; public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission"; + public const string PushSyncOrgKeysOnRevokeRestore = "pm-17168-push-sync-org-keys-on-revoke-restore"; /* Tools Team */ public const string ItemShare = "item-share"; diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index cd680f2ef0..f4440a65a3 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -19,6 +19,7 @@ using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Mail; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; +using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -1450,76 +1451,174 @@ OrganizationUserInvite invite, SutProvider sutProvider) [OrganizationUser] OrganizationUser organizationUser, SutProvider sutProvider) { RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); await sutProvider.Sut.RevokeUserAsync(organizationUser, owner.Id); - await organizationUserRepository.Received().RevokeAsync(organizationUser.Id); - await eventService.Received() + await sutProvider.GetDependency() + .Received(1) + .RevokeAsync(organizationUser.Id); + await sutProvider.GetDependency() + .Received(1) .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked); } + [Theory, BitAutoData] + public async Task RevokeUser_WithPushSyncOrgKeysOnRevokeRestoreEnabled_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, + [OrganizationUser] OrganizationUser organizationUser, SutProvider sutProvider) + { + RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) + .Returns(true); + + await sutProvider.Sut.RevokeUserAsync(organizationUser, owner.Id); + + await sutProvider.GetDependency() + .Received(1) + .RevokeAsync(organizationUser.Id); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked); + await sutProvider.GetDependency() + .Received(1) + .PushSyncOrgKeysAsync(organizationUser.UserId!.Value); + } + [Theory, BitAutoData] public async Task RevokeUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider sutProvider) { RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); await sutProvider.Sut.RevokeUserAsync(organizationUser, eventSystemUser); - await organizationUserRepository.Received().RevokeAsync(organizationUser.Id); - await eventService.Received() + await sutProvider.GetDependency() + .Received(1) + .RevokeAsync(organizationUser.Id); + await sutProvider.GetDependency() + .Received(1) .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, eventSystemUser); } + [Theory, BitAutoData] + public async Task RevokeUser_WithEventSystemUser_WithPushSyncOrgKeysOnRevokeRestoreEnabled_Success(Organization organization, [OrganizationUser] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider sutProvider) + { + RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) + .Returns(true); + + await sutProvider.Sut.RevokeUserAsync(organizationUser, eventSystemUser); + + await sutProvider.GetDependency() + .Received(1) + .RevokeAsync(organizationUser.Id); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, eventSystemUser); + await sutProvider.GetDependency() + .Received(1) + .PushSyncOrgKeysAsync(organizationUser.UserId!.Value); + } + [Theory, BitAutoData] public async Task RestoreUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider sutProvider) { RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id); - await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited); - await eventService.Received() + await sutProvider.GetDependency() + .Received(1) + .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited); + await sutProvider.GetDependency() + .Received(1) .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored); } + [Theory, BitAutoData] + public async Task RestoreUser_WithPushSyncOrgKeysOnRevokeRestoreEnabled_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider sutProvider) + { + RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) + .Returns(true); + + await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id); + + await sutProvider.GetDependency() + .Received(1) + .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored); + await sutProvider.GetDependency() + .Received(1) + .PushSyncOrgKeysAsync(organizationUser.UserId!.Value); + } + [Theory, BitAutoData] public async Task RestoreUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider sutProvider) { RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); await sutProvider.Sut.RestoreUserAsync(organizationUser, eventSystemUser); - await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited); - await eventService.Received() + await sutProvider.GetDependency() + .Received(1) + .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited); + await sutProvider.GetDependency() + .Received(1) .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, eventSystemUser); } + [Theory, BitAutoData] + public async Task RestoreUser_WithEventSystemUser_WithPushSyncOrgKeysOnRevokeRestoreEnabled_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider sutProvider) + { + RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) + .Returns(true); + + await sutProvider.Sut.RestoreUserAsync(organizationUser, eventSystemUser); + + await sutProvider.GetDependency() + .Received(1) + .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, eventSystemUser); + await sutProvider.GetDependency() + .Received(1) + .PushSyncOrgKeysAsync(organizationUser.UserId!.Value); + } + [Theory, BitAutoData] public async Task RestoreUser_RestoreThemselves_Fails(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider sutProvider) { organizationUser.UserId = owner.Id; RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); Assert.Contains("you cannot restore yourself", exception.Message.ToLowerInvariant()); - await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any(), Arg.Any()); - await eventService.DidNotReceiveWithAnyArgs() + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RestoreAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .PushSyncOrgKeysAsync(Arg.Any()); } [Theory] @@ -1531,17 +1630,21 @@ OrganizationUserInvite invite, SutProvider sutProvider) { restoringUser.Type = restoringUserType; RestoreRevokeUser_Setup(organization, restoringUser, organizationUser, sutProvider); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RestoreUserAsync(organizationUser, restoringUser.Id)); Assert.Contains("only owners can restore other owners", exception.Message.ToLowerInvariant()); - await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any(), Arg.Any()); - await eventService.DidNotReceiveWithAnyArgs() + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RestoreAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .PushSyncOrgKeysAsync(Arg.Any()); } [Theory] @@ -1553,17 +1656,21 @@ OrganizationUserInvite invite, SutProvider sutProvider) { organizationUser.Status = userStatus; RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); Assert.Contains("already active", exception.Message.ToLowerInvariant()); - await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any(), Arg.Any()); - await eventService.DidNotReceiveWithAnyArgs() + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RestoreAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .PushSyncOrgKeysAsync(Arg.Any()); } [Theory, BitAutoData] @@ -1575,8 +1682,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) { organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); sutProvider.GetDependency() .AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any()) @@ -1591,9 +1696,15 @@ OrganizationUserInvite invite, SutProvider sutProvider) 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() + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RestoreAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .PushSyncOrgKeysAsync(Arg.Any()); } [Theory, BitAutoData] @@ -1610,8 +1721,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, false) }); RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); sutProvider.GetDependency() .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any()) @@ -1626,9 +1735,15 @@ OrganizationUserInvite invite, SutProvider sutProvider) 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() + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RestoreAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .PushSyncOrgKeysAsync(Arg.Any()); } [Theory, BitAutoData] @@ -1640,8 +1755,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) { organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); sutProvider.GetDependency() .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any()) @@ -1652,8 +1765,11 @@ OrganizationUserInvite invite, SutProvider sutProvider) await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id); - await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed); - await eventService.Received() + await sutProvider.GetDependency() + .Received(1) + .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed); + await sutProvider.GetDependency() + .Received(1) .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored); } @@ -1668,10 +1784,10 @@ OrganizationUserInvite invite, 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() + .GetManyByUserAsync(organizationUser.UserId.Value) + .Returns(new[] { organizationUser, secondOrganizationUser }); sutProvider.GetDependency() .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any()) .Returns(new[] @@ -1688,9 +1804,15 @@ OrganizationUserInvite invite, SutProvider sutProvider) 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() + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RestoreAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .PushSyncOrgKeysAsync(Arg.Any()); } [Theory, BitAutoData] @@ -1704,11 +1826,8 @@ OrganizationUserInvite invite, 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(); - var twoFactorIsEnabledQuery = sutProvider.GetDependency(); - twoFactorIsEnabledQuery + sutProvider.GetDependency() .TwoFactorIsEnabledAsync(Arg.Is>(i => i.Contains(organizationUser.UserId.Value))) .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) }); @@ -1725,9 +1844,15 @@ OrganizationUserInvite invite, SutProvider sutProvider) 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() + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RestoreAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .PushSyncOrgKeysAsync(Arg.Any()); } [Theory, BitAutoData] @@ -1741,10 +1866,10 @@ OrganizationUserInvite invite, 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() + .GetManyByUserAsync(organizationUser.UserId.Value) + .Returns(new[] { organizationUser, secondOrganizationUser }); sutProvider.GetDependency() .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any()) .Returns(new[] @@ -1768,9 +1893,15 @@ OrganizationUserInvite invite, SutProvider sutProvider) 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() + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RestoreAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .PushSyncOrgKeysAsync(Arg.Any()); } [Theory, BitAutoData] @@ -1783,8 +1914,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) organizationUser.Email = null; RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); sutProvider.GetDependency() .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any()) @@ -1799,9 +1928,15 @@ OrganizationUserInvite invite, SutProvider sutProvider) 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() + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RestoreAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .PushSyncOrgKeysAsync(Arg.Any()); } [Theory, BitAutoData] @@ -1813,22 +1948,22 @@ OrganizationUserInvite invite, SutProvider sutProvider) { organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); - var twoFactorIsEnabledQuery = sutProvider.GetDependency(); sutProvider.GetDependency() .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any()) .Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } }); - twoFactorIsEnabledQuery + sutProvider.GetDependency() .TwoFactorIsEnabledAsync(Arg.Is>(i => i.Contains(organizationUser.UserId.Value))) .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) }); await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id); - await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed); - await eventService.Received() + await sutProvider.GetDependency() + .Received(1) + .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed); + await sutProvider.GetDependency() + .Received(1) .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored); } From 17f5c97891ad983687455b3a46006d15c8a29c7a Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Thu, 6 Feb 2025 08:13:17 -0600 Subject: [PATCH 107/148] PM-6939 - Onyx Integration into freshdesk controller (#5365) --- src/Billing/Billing.csproj | 3 + src/Billing/BillingSettings.cs | 7 + src/Billing/Controllers/BitPayController.cs | 1 + .../Controllers/FreshdeskController.cs | 137 +++++++++++++++ .../Models/FreshdeskViewTicketModel.cs | 44 +++++ .../OnyxAnswerWithCitationRequestModel.cs | 54 ++++++ .../OnyxAnswerWithCitationResponseModel.cs | 30 ++++ src/Billing/Startup.cs | 15 ++ src/Billing/appsettings.json | 6 +- .../Controllers/FreshdeskControllerTests.cs | 156 +++++++++++++++++- 10 files changed, 451 insertions(+), 2 deletions(-) create mode 100644 src/Billing/Models/FreshdeskViewTicketModel.cs create mode 100644 src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs create mode 100644 src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index b30d987a95..f32eccfe8c 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -10,5 +10,8 @@ + + + diff --git a/src/Billing/BillingSettings.cs b/src/Billing/BillingSettings.cs index 91ea8f1221..ffe73808d4 100644 --- a/src/Billing/BillingSettings.cs +++ b/src/Billing/BillingSettings.cs @@ -12,6 +12,7 @@ public class BillingSettings public virtual FreshDeskSettings FreshDesk { get; set; } = new FreshDeskSettings(); public virtual string FreshsalesApiKey { get; set; } public virtual PayPalSettings PayPal { get; set; } = new PayPalSettings(); + public virtual OnyxSettings Onyx { get; set; } = new OnyxSettings(); public class PayPalSettings { @@ -31,4 +32,10 @@ public class BillingSettings public virtual string UserFieldName { get; set; } public virtual string OrgFieldName { get; set; } } + + public class OnyxSettings + { + public virtual string ApiKey { get; set; } + public virtual string BaseUrl { get; set; } + } } diff --git a/src/Billing/Controllers/BitPayController.cs b/src/Billing/Controllers/BitPayController.cs index 026909aed1..4caf57aa20 100644 --- a/src/Billing/Controllers/BitPayController.cs +++ b/src/Billing/Controllers/BitPayController.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.Options; namespace Bit.Billing.Controllers; [Route("bitpay")] +[ApiExplorerSettings(IgnoreApi = true)] public class BitPayController : Controller { private readonly BillingSettings _billingSettings; diff --git a/src/Billing/Controllers/FreshdeskController.cs b/src/Billing/Controllers/FreshdeskController.cs index 7aeb60a67f..4bf6b7bad4 100644 --- a/src/Billing/Controllers/FreshdeskController.cs +++ b/src/Billing/Controllers/FreshdeskController.cs @@ -1,6 +1,8 @@ using System.ComponentModel.DataAnnotations; +using System.Net.Http.Headers; using System.Reflection; using System.Text; +using System.Text.Json; using System.Web; using Bit.Billing.Models; using Bit.Core.Repositories; @@ -142,6 +144,121 @@ public class FreshdeskController : Controller } } + [HttpPost("webhook-onyx-ai")] + public async Task PostWebhookOnyxAi([FromQuery, Required] string key, + [FromBody, Required] FreshdeskWebhookModel model) + { + // ensure that the key is from Freshdesk + if (!IsValidRequestFromFreshdesk(key)) + { + return new BadRequestResult(); + } + + // get ticket info from Freshdesk + var getTicketRequest = new HttpRequestMessage(HttpMethod.Get, + string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}", model.TicketId)); + var getTicketResponse = await CallFreshdeskApiAsync(getTicketRequest); + + // check if we have a valid response from freshdesk + if (getTicketResponse.StatusCode != System.Net.HttpStatusCode.OK) + { + _logger.LogError("Error getting ticket info from Freshdesk. Ticket Id: {0}. Status code: {1}", + model.TicketId, getTicketResponse.StatusCode); + return BadRequest("Failed to retrieve ticket info from Freshdesk"); + } + + // extract info from the response + var ticketInfo = await ExtractTicketInfoFromResponse(getTicketResponse); + if (ticketInfo == null) + { + return BadRequest("Failed to extract ticket info from Freshdesk response"); + } + + // create the onyx `answer-with-citation` request + var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(ticketInfo.DescriptionText); + var onyxRequest = new HttpRequestMessage(HttpMethod.Post, + string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl)) + { + Content = JsonContent.Create(onyxRequestModel, mediaType: new MediaTypeHeaderValue("application/json")), + }; + var (_, onyxJsonResponse) = await CallOnyxApi(onyxRequest); + + // the CallOnyxApi will return a null if we have an error response + if (onyxJsonResponse?.Answer == null || !string.IsNullOrEmpty(onyxJsonResponse?.ErrorMsg)) + { + return BadRequest( + string.Format("Failed to get a valid response from Onyx API. Response: {0}", + JsonSerializer.Serialize(onyxJsonResponse ?? new OnyxAnswerWithCitationResponseModel()))); + } + + // add the answer as a note to the ticket + await AddAnswerNoteToTicketAsync(onyxJsonResponse.Answer, model.TicketId); + + return Ok(); + } + + private bool IsValidRequestFromFreshdesk(string key) + { + if (string.IsNullOrWhiteSpace(key) + || !CoreHelpers.FixedTimeEquals(key, _billingSettings.FreshDesk.WebhookKey)) + { + return false; + } + + return true; + } + + private async Task AddAnswerNoteToTicketAsync(string note, string ticketId) + { + // if there is no content, then we don't need to add a note + if (string.IsNullOrWhiteSpace(note)) + { + return; + } + + var noteBody = new Dictionary + { + { "body", $"Onyx AI:
    {note}
" }, + { "private", true } + }; + + var noteRequest = new HttpRequestMessage(HttpMethod.Post, + string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId)) + { + Content = JsonContent.Create(noteBody), + }; + + var addNoteResponse = await CallFreshdeskApiAsync(noteRequest); + if (addNoteResponse.StatusCode != System.Net.HttpStatusCode.Created) + { + _logger.LogError("Error adding note to Freshdesk ticket. Ticket Id: {0}. Status: {1}", + ticketId, addNoteResponse.ToString()); + } + } + + private async Task ExtractTicketInfoFromResponse(HttpResponseMessage getTicketResponse) + { + var responseString = string.Empty; + try + { + responseString = await getTicketResponse.Content.ReadAsStringAsync(); + var ticketInfo = JsonSerializer.Deserialize(responseString, + options: new System.Text.Json.JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }); + + return ticketInfo; + } + catch (System.Exception ex) + { + _logger.LogError("Error deserializing ticket info from Freshdesk response. Response: {0}. Exception {1}", + responseString, ex.ToString()); + } + + return null; + } + private async Task CallFreshdeskApiAsync(HttpRequestMessage request, int retriedCount = 0) { try @@ -166,6 +283,26 @@ public class FreshdeskController : Controller return await CallFreshdeskApiAsync(request, retriedCount++); } + private async Task<(HttpResponseMessage, T)> CallOnyxApi(HttpRequestMessage request) + { + var httpClient = _httpClientFactory.CreateClient("OnyxApi"); + var response = await httpClient.SendAsync(request); + + if (response.StatusCode != System.Net.HttpStatusCode.OK) + { + _logger.LogError("Error calling Onyx AI API. Status code: {0}. Response {1}", + response.StatusCode, JsonSerializer.Serialize(response)); + return (null, default); + } + var responseStr = await response.Content.ReadAsStringAsync(); + var responseJson = JsonSerializer.Deserialize(responseStr, options: new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }); + + return (response, responseJson); + } + private TAttribute GetAttribute(Enum enumValue) where TAttribute : Attribute { return enumValue.GetType().GetMember(enumValue.ToString()).First().GetCustomAttribute(); diff --git a/src/Billing/Models/FreshdeskViewTicketModel.cs b/src/Billing/Models/FreshdeskViewTicketModel.cs new file mode 100644 index 0000000000..2aa6eff94d --- /dev/null +++ b/src/Billing/Models/FreshdeskViewTicketModel.cs @@ -0,0 +1,44 @@ +namespace Bit.Billing.Models; + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +public class FreshdeskViewTicketModel +{ + [JsonPropertyName("spam")] + public bool? Spam { get; set; } + + [JsonPropertyName("priority")] + public int? Priority { get; set; } + + [JsonPropertyName("source")] + public int? Source { get; set; } + + [JsonPropertyName("status")] + public int? Status { get; set; } + + [JsonPropertyName("subject")] + public string Subject { get; set; } + + [JsonPropertyName("support_email")] + public string SupportEmail { get; set; } + + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("description_text")] + public string DescriptionText { get; set; } + + [JsonPropertyName("created_at")] + public DateTime CreatedAt { get; set; } + + [JsonPropertyName("updated_at")] + public DateTime UpdatedAt { get; set; } + + [JsonPropertyName("tags")] + public List Tags { get; set; } +} diff --git a/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs b/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs new file mode 100644 index 0000000000..e7bd29b2f5 --- /dev/null +++ b/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs @@ -0,0 +1,54 @@ + +using System.Text.Json.Serialization; + +namespace Bit.Billing.Models; + +public class OnyxAnswerWithCitationRequestModel +{ + [JsonPropertyName("messages")] + public List Messages { get; set; } + + [JsonPropertyName("persona_id")] + public int PersonaId { get; set; } = 1; + + [JsonPropertyName("prompt_id")] + public int PromptId { get; set; } = 1; + + [JsonPropertyName("retrieval_options")] + public RetrievalOptions RetrievalOptions { get; set; } + + public OnyxAnswerWithCitationRequestModel(string message) + { + message = message.Replace(Environment.NewLine, " ").Replace('\r', ' ').Replace('\n', ' '); + Messages = new List() { new Message() { MessageText = message } }; + RetrievalOptions = new RetrievalOptions(); + } +} + +public class Message +{ + [JsonPropertyName("message")] + public string MessageText { get; set; } + + [JsonPropertyName("sender")] + public string Sender { get; set; } = "user"; +} + +public class RetrievalOptions +{ + [JsonPropertyName("run_search")] + public string RunSearch { get; set; } = RetrievalOptionsRunSearch.Auto; + + [JsonPropertyName("real_time")] + public bool RealTime { get; set; } = true; + + [JsonPropertyName("limit")] + public int? Limit { get; set; } = 3; +} + +public class RetrievalOptionsRunSearch +{ + public const string Always = "always"; + public const string Never = "never"; + public const string Auto = "auto"; +} diff --git a/src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs b/src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs new file mode 100644 index 0000000000..e85ee9a674 --- /dev/null +++ b/src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs @@ -0,0 +1,30 @@ +using System.Text.Json.Serialization; + +namespace Bit.Billing.Models; + +public class OnyxAnswerWithCitationResponseModel +{ + [JsonPropertyName("answer")] + public string Answer { get; set; } + + [JsonPropertyName("rephrase")] + public string Rephrase { get; set; } + + [JsonPropertyName("citations")] + public List Citations { get; set; } + + [JsonPropertyName("llm_selected_doc_indices")] + public List LlmSelectedDocIndices { get; set; } + + [JsonPropertyName("error_msg")] + public string ErrorMsg { get; set; } +} + +public class Citation +{ + [JsonPropertyName("citation_num")] + public int CitationNum { get; set; } + + [JsonPropertyName("document_id")] + public string DocumentId { get; set; } +} diff --git a/src/Billing/Startup.cs b/src/Billing/Startup.cs index 2d2f109e77..e9f2f53488 100644 --- a/src/Billing/Startup.cs +++ b/src/Billing/Startup.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Net.Http.Headers; using Bit.Billing.Services; using Bit.Billing.Services.Implementations; using Bit.Core.Billing.Extensions; @@ -34,6 +35,7 @@ public class Startup // Settings var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment); services.Configure(Configuration.GetSection("BillingSettings")); + var billingSettings = Configuration.GetSection("BillingSettings").Get(); // Stripe Billing StripeConfiguration.ApiKey = globalSettings.Stripe.ApiKey; @@ -97,6 +99,10 @@ public class Startup // Set up HttpClients services.AddHttpClient("FreshdeskApi"); + services.AddHttpClient("OnyxApi", client => + { + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", billingSettings.Onyx.ApiKey); + }); services.AddScoped(); services.AddScoped(); @@ -112,6 +118,10 @@ public class Startup // Jobs service Jobs.JobsHostedService.AddJobsServices(services); services.AddHostedService(); + + // Swagger + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(); } public void Configure( @@ -128,6 +138,11 @@ public class Startup if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "Billing API V1"); + }); } app.UseStaticFiles(); diff --git a/src/Billing/appsettings.json b/src/Billing/appsettings.json index 84a67434f5..2a2864b246 100644 --- a/src/Billing/appsettings.json +++ b/src/Billing/appsettings.json @@ -73,6 +73,10 @@ "region": "US", "userFieldName": "cf_user", "orgFieldName": "cf_org" - } + }, + "onyx": { + "apiKey": "SECRET", + "baseUrl": "https://cloud.onyx.app/api" + } } } diff --git a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs b/test/Billing.Test/Controllers/FreshdeskControllerTests.cs index f07c64dad9..26ce310b9c 100644 --- a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs +++ b/test/Billing.Test/Controllers/FreshdeskControllerTests.cs @@ -1,4 +1,5 @@ -using Bit.Billing.Controllers; +using System.Text.Json; +using Bit.Billing.Controllers; using Bit.Billing.Models; using Bit.Core.AdminConsole.Entities; using Bit.Core.Entities; @@ -70,6 +71,159 @@ public class FreshdeskControllerTests _ = mockHttpMessageHandler.Received(1).Send(Arg.Is(m => m.Method == HttpMethod.Post && m.RequestUri.ToString().EndsWith($"{model.TicketId}/notes")), Arg.Any()); } + [Theory] + [BitAutoData((string)null, null)] + [BitAutoData((string)null)] + [BitAutoData(WebhookKey, null)] + public async Task PostWebhookOnyxAi_InvalidWebhookKey_results_in_BadRequest( + string freshdeskWebhookKey, FreshdeskWebhookModel model, + BillingSettings billingSettings, SutProvider sutProvider) + { + sutProvider.GetDependency>() + .Value.FreshDesk.WebhookKey.Returns(billingSettings.FreshDesk.WebhookKey); + + var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model); + + var statusCodeResult = Assert.IsAssignableFrom(response); + Assert.Equal(StatusCodes.Status400BadRequest, statusCodeResult.StatusCode); + } + + [Theory] + [BitAutoData(WebhookKey)] + public async Task PostWebhookOnyxAi_invalid_ticketid_results_in_BadRequest( + string freshdeskWebhookKey, FreshdeskWebhookModel model, SutProvider sutProvider) + { + sutProvider.GetDependency>() + .Value.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey); + + var mockHttpMessageHandler = Substitute.ForPartsOf(); + var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest); + mockHttpMessageHandler.Send(Arg.Any(), Arg.Any()) + .Returns(mockResponse); + var httpClient = new HttpClient(mockHttpMessageHandler); + + sutProvider.GetDependency().CreateClient("FreshdeskApi").Returns(httpClient); + + var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model); + + var result = Assert.IsAssignableFrom(response); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + } + + [Theory] + [BitAutoData(WebhookKey)] + public async Task PostWebhookOnyxAi_invalid_freshdesk_response_results_in_BadRequest( + string freshdeskWebhookKey, FreshdeskWebhookModel model, + SutProvider sutProvider) + { + sutProvider.GetDependency>() + .Value.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey); + + var mockHttpMessageHandler = Substitute.ForPartsOf(); + var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent("non json content. expect json deserializer to throw error") + }; + mockHttpMessageHandler.Send(Arg.Any(), Arg.Any()) + .Returns(mockResponse); + var httpClient = new HttpClient(mockHttpMessageHandler); + + sutProvider.GetDependency().CreateClient("FreshdeskApi").Returns(httpClient); + + var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model); + + var result = Assert.IsAssignableFrom(response); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + } + + [Theory] + [BitAutoData(WebhookKey)] + public async Task PostWebhookOnyxAi_invalid_onyx_response_results_in_BadRequest( + string freshdeskWebhookKey, FreshdeskWebhookModel model, + FreshdeskViewTicketModel freshdeskTicketInfo, SutProvider sutProvider) + { + var billingSettings = sutProvider.GetDependency>().Value; + billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey); + billingSettings.Onyx.BaseUrl.Returns("http://simulate-onyx-api.com/api"); + + // mocking freshdesk Api request for ticket info + var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf(); + var mockFreshdeskResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(JsonSerializer.Serialize(freshdeskTicketInfo)) + }; + mockFreshdeskHttpMessageHandler.Send(Arg.Any(), Arg.Any()) + .Returns(mockFreshdeskResponse); + var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler); + + // mocking Onyx api response given a ticket description + var mockOnyxHttpMessageHandler = Substitute.ForPartsOf(); + var mockOnyxResponse = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest); + mockOnyxHttpMessageHandler.Send(Arg.Any(), Arg.Any()) + .Returns(mockOnyxResponse); + var onyxHttpClient = new HttpClient(mockOnyxHttpMessageHandler); + + sutProvider.GetDependency().CreateClient("FreshdeskApi").Returns(freshdeskHttpClient); + sutProvider.GetDependency().CreateClient("OnyxApi").Returns(onyxHttpClient); + + var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model); + + var result = Assert.IsAssignableFrom(response); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + } + + [Theory] + [BitAutoData(WebhookKey)] + public async Task PostWebhookOnyxAi_success( + string freshdeskWebhookKey, FreshdeskWebhookModel model, + FreshdeskViewTicketModel freshdeskTicketInfo, + OnyxAnswerWithCitationResponseModel onyxResponse, + SutProvider sutProvider) + { + var billingSettings = sutProvider.GetDependency>().Value; + billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey); + billingSettings.Onyx.BaseUrl.Returns("http://simulate-onyx-api.com/api"); + + // mocking freshdesk Api request for ticket info (GET) + var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf(); + var mockFreshdeskResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(JsonSerializer.Serialize(freshdeskTicketInfo)) + }; + mockFreshdeskHttpMessageHandler.Send( + Arg.Is(_ => _.Method == HttpMethod.Get), + Arg.Any()) + .Returns(mockFreshdeskResponse); + + // mocking freshdesk api add note request (POST) + var mockFreshdeskAddNoteResponse = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest); + mockFreshdeskHttpMessageHandler.Send( + Arg.Is(_ => _.Method == HttpMethod.Post), + Arg.Any()) + .Returns(mockFreshdeskAddNoteResponse); + var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler); + + + // mocking Onyx api response given a ticket description + var mockOnyxHttpMessageHandler = Substitute.ForPartsOf(); + onyxResponse.ErrorMsg = string.Empty; + var mockOnyxResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(JsonSerializer.Serialize(onyxResponse)) + }; + mockOnyxHttpMessageHandler.Send(Arg.Any(), Arg.Any()) + .Returns(mockOnyxResponse); + var onyxHttpClient = new HttpClient(mockOnyxHttpMessageHandler); + + sutProvider.GetDependency().CreateClient("FreshdeskApi").Returns(freshdeskHttpClient); + sutProvider.GetDependency().CreateClient("OnyxApi").Returns(onyxHttpClient); + + var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model); + + var result = Assert.IsAssignableFrom(response); + Assert.Equal(StatusCodes.Status200OK, result.StatusCode); + } + public class MockHttpMessageHandler : HttpMessageHandler { protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) From bc27ec2b9b800c7dbb5ffd9e6454d44df144ef3c Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Thu, 6 Feb 2025 15:15:36 +0100 Subject: [PATCH 108/148] =?UTF-8?q?[PM-12765]=20Change=20error=20message?= =?UTF-8?q?=20when=20subscription=20canceled=20and=20attemp=E2=80=A6=20(#5?= =?UTF-8?q?346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/Implementations/OrganizationService.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 6194aea6c7..6f4aba4882 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -1294,6 +1294,12 @@ public class OrganizationService : IOrganizationService } } + var subscription = await _paymentService.GetSubscriptionAsync(organization); + if (subscription?.Subscription?.Status == StripeConstants.SubscriptionStatus.Canceled) + { + return (false, "You do not have an active subscription. Reinstate your subscription to make changes"); + } + if (organization.Seats.HasValue && organization.MaxAutoscaleSeats.HasValue && organization.MaxAutoscaleSeats.Value < organization.Seats.Value + seatsToAdd) @@ -1301,12 +1307,6 @@ 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 678d5d5d632447ac3431781d8232971eab713edc Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Thu, 6 Feb 2025 16:34:22 +0100 Subject: [PATCH 109/148] [PM-18028] Attempting to enable automatic tax on customer with invalid location (#5374) --- .../Implementations/UpcomingInvoiceHandler.cs | 13 +++--- .../Billing/Extensions/CustomerExtensions.cs | 16 +++++++ .../SubscriptionCreateOptionsExtensions.cs | 26 +++++++++++ .../SubscriptionUpdateOptionsExtensions.cs | 35 +++++++++++++++ .../UpcomingInvoiceOptionsExtensions.cs | 35 +++++++++++++++ .../PremiumUserBillingService.cs | 2 +- .../Implementations/SubscriberService.cs | 20 ++++++--- .../Implementations/StripePaymentService.cs | 43 +++++++++---------- 8 files changed, 155 insertions(+), 35 deletions(-) create mode 100644 src/Core/Billing/Extensions/CustomerExtensions.cs create mode 100644 src/Core/Billing/Extensions/SubscriptionCreateOptionsExtensions.cs create mode 100644 src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs create mode 100644 src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs diff --git a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs index c52c03b6aa..409bd0d18b 100644 --- a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs +++ b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs @@ -1,6 +1,7 @@ using Bit.Billing.Constants; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Extensions; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -160,16 +161,16 @@ public class UpcomingInvoiceHandler : IUpcomingInvoiceHandler private async Task TryEnableAutomaticTaxAsync(Subscription subscription) { - if (subscription.AutomaticTax.Enabled) + var customerGetOptions = new CustomerGetOptions { Expand = ["tax"] }; + var customer = await _stripeFacade.GetCustomer(subscription.CustomerId, customerGetOptions); + + var subscriptionUpdateOptions = new SubscriptionUpdateOptions(); + + if (!subscriptionUpdateOptions.EnableAutomaticTax(customer, subscription)) { return subscription; } - var subscriptionUpdateOptions = new SubscriptionUpdateOptions - { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } - }; - return await _stripeFacade.UpdateSubscription(subscription.Id, subscriptionUpdateOptions); } diff --git a/src/Core/Billing/Extensions/CustomerExtensions.cs b/src/Core/Billing/Extensions/CustomerExtensions.cs new file mode 100644 index 0000000000..62f1a5055c --- /dev/null +++ b/src/Core/Billing/Extensions/CustomerExtensions.cs @@ -0,0 +1,16 @@ +using Bit.Core.Billing.Constants; +using Stripe; + +namespace Bit.Core.Billing.Extensions; + +public static class CustomerExtensions +{ + + /// + /// Determines if a Stripe customer supports automatic tax + /// + /// + /// + public static bool HasTaxLocationVerified(this Customer customer) => + customer?.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported; +} diff --git a/src/Core/Billing/Extensions/SubscriptionCreateOptionsExtensions.cs b/src/Core/Billing/Extensions/SubscriptionCreateOptionsExtensions.cs new file mode 100644 index 0000000000..d76a0553a3 --- /dev/null +++ b/src/Core/Billing/Extensions/SubscriptionCreateOptionsExtensions.cs @@ -0,0 +1,26 @@ +using Stripe; + +namespace Bit.Core.Billing.Extensions; + +public static class SubscriptionCreateOptionsExtensions +{ + /// + /// Attempts to enable automatic tax for given new subscription options. + /// + /// + /// The existing customer. + /// Returns true when successful, false when conditions are not met. + public static bool EnableAutomaticTax(this SubscriptionCreateOptions options, Customer customer) + { + // We might only need to check the automatic tax status. + if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country)) + { + return false; + } + + options.DefaultTaxRates = []; + options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; + + return true; + } +} diff --git a/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs b/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs new file mode 100644 index 0000000000..d70af78fa8 --- /dev/null +++ b/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs @@ -0,0 +1,35 @@ +using Stripe; + +namespace Bit.Core.Billing.Extensions; + +public static class SubscriptionUpdateOptionsExtensions +{ + /// + /// Attempts to enable automatic tax for given subscription options. + /// + /// + /// The existing customer to which the subscription belongs. + /// The existing subscription. + /// Returns true when successful, false when conditions are not met. + public static bool EnableAutomaticTax( + this SubscriptionUpdateOptions options, + Customer customer, + Subscription subscription) + { + if (subscription.AutomaticTax.Enabled) + { + return false; + } + + // We might only need to check the automatic tax status. + if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country)) + { + return false; + } + + options.DefaultTaxRates = []; + options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; + + return true; + } +} diff --git a/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs b/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs new file mode 100644 index 0000000000..88df5638c9 --- /dev/null +++ b/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs @@ -0,0 +1,35 @@ +using Stripe; + +namespace Bit.Core.Billing.Extensions; + +public static class UpcomingInvoiceOptionsExtensions +{ + /// + /// Attempts to enable automatic tax for given upcoming invoice options. + /// + /// + /// The existing customer to which the upcoming invoice belongs. + /// The existing subscription to which the upcoming invoice belongs. + /// Returns true when successful, false when conditions are not met. + public static bool EnableAutomaticTax( + this UpcomingInvoiceOptions options, + Customer customer, + Subscription subscription) + { + if (subscription != null && subscription.AutomaticTax.Enabled) + { + return false; + } + + // We might only need to check the automatic tax status. + if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country)) + { + return false; + } + + options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }; + options.SubscriptionDefaultTaxRates = []; + + return true; + } +} diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index ed841c9576..99815c0557 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -258,7 +258,7 @@ public class PremiumUserBillingService( { AutomaticTax = new SubscriptionAutomaticTaxOptions { - Enabled = true + Enabled = customer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported, }, CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, Customer = customer.Id, diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index f4cf22ac19..b2dca19e80 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -661,11 +661,21 @@ public class SubscriberService( } } - await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId, - new SubscriptionUpdateOptions - { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, - }); + if (SubscriberIsEligibleForAutomaticTax(subscriber, customer)) + { + await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId, + new SubscriptionUpdateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } + }); + } + + return; + + bool SubscriberIsEligibleForAutomaticTax(ISubscriber localSubscriber, Customer localCustomer) + => !string.IsNullOrEmpty(localSubscriber.GatewaySubscriptionId) && + (localCustomer.Subscriptions?.Any(sub => sub.Id == localSubscriber.GatewaySubscriptionId && !sub.AutomaticTax.Enabled) ?? false) && + localCustomer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported; } public async Task VerifyBankAccount( diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index fb5c7364a5..553c1c65a8 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -177,7 +177,7 @@ public class StripePaymentService : IPaymentService customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions); subCreateOptions.AddExpand("latest_invoice.payment_intent"); subCreateOptions.Customer = customer.Id; - subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; + subCreateOptions.EnableAutomaticTax(customer); subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions); if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null) @@ -358,10 +358,9 @@ public class StripePaymentService : IPaymentService customer = await _stripeAdapter.CustomerUpdateAsync(org.GatewayCustomerId, customerUpdateOptions); } - var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade) - { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } - }; + var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade); + + subCreateOptions.EnableAutomaticTax(customer); var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions); @@ -520,10 +519,6 @@ public class StripePaymentService : IPaymentService var customerCreateOptions = new CustomerCreateOptions { - Tax = new CustomerTaxOptions - { - ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately - }, Description = user.Name, Email = user.Email, Metadata = stripeCustomerMetadata, @@ -561,7 +556,6 @@ public class StripePaymentService : IPaymentService var subCreateOptions = new SubscriptionCreateOptions { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, Customer = customer.Id, Items = [], Metadata = new Dictionary @@ -581,10 +575,12 @@ public class StripePaymentService : IPaymentService subCreateOptions.Items.Add(new SubscriptionItemOptions { Plan = StoragePlanId, - Quantity = additionalStorageGb, + Quantity = additionalStorageGb }); } + subCreateOptions.EnableAutomaticTax(customer); + var subscription = await ChargeForNewSubscriptionAsync(user, customer, createdStripeCustomer, stripePaymentMethod, paymentMethodType, subCreateOptions, braintreeCustomer); @@ -622,7 +618,10 @@ public class StripePaymentService : IPaymentService SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items) }); - previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true }; + if (customer.HasTaxLocationVerified()) + { + previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true }; + } if (previewInvoice.AmountDue > 0) { @@ -680,12 +679,10 @@ public class StripePaymentService : IPaymentService Customer = customer.Id, SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items), SubscriptionDefaultTaxRates = subCreateOptions.DefaultTaxRates, - AutomaticTax = new InvoiceAutomaticTaxOptions - { - Enabled = true - } }; + upcomingInvoiceOptions.EnableAutomaticTax(customer, null); + var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions); if (previewInvoice.AmountDue > 0) @@ -804,11 +801,7 @@ public class StripePaymentService : IPaymentService Items = updatedItemOptions, ProrationBehavior = invoiceNow ? Constants.AlwaysInvoice : Constants.CreateProrations, DaysUntilDue = daysUntilDue ?? 1, - CollectionMethod = "send_invoice", - AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = true - } + CollectionMethod = "send_invoice" }; if (!invoiceNow && isAnnualPlan && sub.Status.Trim() != "trialing") { @@ -816,6 +809,8 @@ public class StripePaymentService : IPaymentService new SubscriptionPendingInvoiceItemIntervalOptions { Interval = "month" }; } + subUpdateOptions.EnableAutomaticTax(sub.Customer, sub); + if (!subscriptionUpdate.UpdateNeeded(sub)) { // No need to update subscription, quantity matches @@ -1500,11 +1495,13 @@ public class StripePaymentService : IPaymentService if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId) && customer.Subscriptions.Any(sub => sub.Id == subscriber.GatewaySubscriptionId && - !sub.AutomaticTax.Enabled)) + !sub.AutomaticTax.Enabled) && + customer.HasTaxLocationVerified()) { var subscriptionUpdateOptions = new SubscriptionUpdateOptions { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, + DefaultTaxRates = [] }; _ = await _stripeAdapter.SubscriptionUpdateAsync( From a1ef07ea69ad39a92931ada2c3c8963b18237020 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Thu, 6 Feb 2025 17:11:20 +0100 Subject: [PATCH 110/148] =?UTF-8?q?Revert=20"[PM-18028]=20Attempting=20to?= =?UTF-8?q?=20enable=20automatic=20tax=20on=20customer=20with=20invali?= =?UTF-8?q?=E2=80=A6"=20(#5375)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 678d5d5d632447ac3431781d8232971eab713edc. --- .../Implementations/UpcomingInvoiceHandler.cs | 13 +++--- .../Billing/Extensions/CustomerExtensions.cs | 16 ------- .../SubscriptionCreateOptionsExtensions.cs | 26 ----------- .../SubscriptionUpdateOptionsExtensions.cs | 35 --------------- .../UpcomingInvoiceOptionsExtensions.cs | 35 --------------- .../PremiumUserBillingService.cs | 2 +- .../Implementations/SubscriberService.cs | 20 +++------ .../Implementations/StripePaymentService.cs | 43 ++++++++++--------- 8 files changed, 35 insertions(+), 155 deletions(-) delete mode 100644 src/Core/Billing/Extensions/CustomerExtensions.cs delete mode 100644 src/Core/Billing/Extensions/SubscriptionCreateOptionsExtensions.cs delete mode 100644 src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs delete mode 100644 src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs diff --git a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs index 409bd0d18b..c52c03b6aa 100644 --- a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs +++ b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs @@ -1,7 +1,6 @@ using Bit.Billing.Constants; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing.Extensions; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -161,16 +160,16 @@ public class UpcomingInvoiceHandler : IUpcomingInvoiceHandler private async Task TryEnableAutomaticTaxAsync(Subscription subscription) { - var customerGetOptions = new CustomerGetOptions { Expand = ["tax"] }; - var customer = await _stripeFacade.GetCustomer(subscription.CustomerId, customerGetOptions); - - var subscriptionUpdateOptions = new SubscriptionUpdateOptions(); - - if (!subscriptionUpdateOptions.EnableAutomaticTax(customer, subscription)) + if (subscription.AutomaticTax.Enabled) { return subscription; } + var subscriptionUpdateOptions = new SubscriptionUpdateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } + }; + return await _stripeFacade.UpdateSubscription(subscription.Id, subscriptionUpdateOptions); } diff --git a/src/Core/Billing/Extensions/CustomerExtensions.cs b/src/Core/Billing/Extensions/CustomerExtensions.cs deleted file mode 100644 index 62f1a5055c..0000000000 --- a/src/Core/Billing/Extensions/CustomerExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Bit.Core.Billing.Constants; -using Stripe; - -namespace Bit.Core.Billing.Extensions; - -public static class CustomerExtensions -{ - - /// - /// Determines if a Stripe customer supports automatic tax - /// - /// - /// - public static bool HasTaxLocationVerified(this Customer customer) => - customer?.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported; -} diff --git a/src/Core/Billing/Extensions/SubscriptionCreateOptionsExtensions.cs b/src/Core/Billing/Extensions/SubscriptionCreateOptionsExtensions.cs deleted file mode 100644 index d76a0553a3..0000000000 --- a/src/Core/Billing/Extensions/SubscriptionCreateOptionsExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Stripe; - -namespace Bit.Core.Billing.Extensions; - -public static class SubscriptionCreateOptionsExtensions -{ - /// - /// Attempts to enable automatic tax for given new subscription options. - /// - /// - /// The existing customer. - /// Returns true when successful, false when conditions are not met. - public static bool EnableAutomaticTax(this SubscriptionCreateOptions options, Customer customer) - { - // We might only need to check the automatic tax status. - if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country)) - { - return false; - } - - options.DefaultTaxRates = []; - options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; - - return true; - } -} diff --git a/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs b/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs deleted file mode 100644 index d70af78fa8..0000000000 --- a/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Stripe; - -namespace Bit.Core.Billing.Extensions; - -public static class SubscriptionUpdateOptionsExtensions -{ - /// - /// Attempts to enable automatic tax for given subscription options. - /// - /// - /// The existing customer to which the subscription belongs. - /// The existing subscription. - /// Returns true when successful, false when conditions are not met. - public static bool EnableAutomaticTax( - this SubscriptionUpdateOptions options, - Customer customer, - Subscription subscription) - { - if (subscription.AutomaticTax.Enabled) - { - return false; - } - - // We might only need to check the automatic tax status. - if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country)) - { - return false; - } - - options.DefaultTaxRates = []; - options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; - - return true; - } -} diff --git a/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs b/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs deleted file mode 100644 index 88df5638c9..0000000000 --- a/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Stripe; - -namespace Bit.Core.Billing.Extensions; - -public static class UpcomingInvoiceOptionsExtensions -{ - /// - /// Attempts to enable automatic tax for given upcoming invoice options. - /// - /// - /// The existing customer to which the upcoming invoice belongs. - /// The existing subscription to which the upcoming invoice belongs. - /// Returns true when successful, false when conditions are not met. - public static bool EnableAutomaticTax( - this UpcomingInvoiceOptions options, - Customer customer, - Subscription subscription) - { - if (subscription != null && subscription.AutomaticTax.Enabled) - { - return false; - } - - // We might only need to check the automatic tax status. - if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country)) - { - return false; - } - - options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }; - options.SubscriptionDefaultTaxRates = []; - - return true; - } -} diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index 99815c0557..ed841c9576 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -258,7 +258,7 @@ public class PremiumUserBillingService( { AutomaticTax = new SubscriptionAutomaticTaxOptions { - Enabled = customer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported, + Enabled = true }, CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, Customer = customer.Id, diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index b2dca19e80..f4cf22ac19 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -661,21 +661,11 @@ public class SubscriberService( } } - if (SubscriberIsEligibleForAutomaticTax(subscriber, customer)) - { - await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId, - new SubscriptionUpdateOptions - { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } - }); - } - - return; - - bool SubscriberIsEligibleForAutomaticTax(ISubscriber localSubscriber, Customer localCustomer) - => !string.IsNullOrEmpty(localSubscriber.GatewaySubscriptionId) && - (localCustomer.Subscriptions?.Any(sub => sub.Id == localSubscriber.GatewaySubscriptionId && !sub.AutomaticTax.Enabled) ?? false) && - localCustomer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported; + await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId, + new SubscriptionUpdateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, + }); } public async Task VerifyBankAccount( diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 553c1c65a8..fb5c7364a5 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -177,7 +177,7 @@ public class StripePaymentService : IPaymentService customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions); subCreateOptions.AddExpand("latest_invoice.payment_intent"); subCreateOptions.Customer = customer.Id; - subCreateOptions.EnableAutomaticTax(customer); + subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions); if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null) @@ -358,9 +358,10 @@ public class StripePaymentService : IPaymentService customer = await _stripeAdapter.CustomerUpdateAsync(org.GatewayCustomerId, customerUpdateOptions); } - var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade); - - subCreateOptions.EnableAutomaticTax(customer); + var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade) + { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } + }; var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions); @@ -519,6 +520,10 @@ public class StripePaymentService : IPaymentService var customerCreateOptions = new CustomerCreateOptions { + Tax = new CustomerTaxOptions + { + ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately + }, Description = user.Name, Email = user.Email, Metadata = stripeCustomerMetadata, @@ -556,6 +561,7 @@ public class StripePaymentService : IPaymentService var subCreateOptions = new SubscriptionCreateOptions { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, Customer = customer.Id, Items = [], Metadata = new Dictionary @@ -575,12 +581,10 @@ public class StripePaymentService : IPaymentService subCreateOptions.Items.Add(new SubscriptionItemOptions { Plan = StoragePlanId, - Quantity = additionalStorageGb + Quantity = additionalStorageGb, }); } - subCreateOptions.EnableAutomaticTax(customer); - var subscription = await ChargeForNewSubscriptionAsync(user, customer, createdStripeCustomer, stripePaymentMethod, paymentMethodType, subCreateOptions, braintreeCustomer); @@ -618,10 +622,7 @@ public class StripePaymentService : IPaymentService SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items) }); - if (customer.HasTaxLocationVerified()) - { - previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true }; - } + previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true }; if (previewInvoice.AmountDue > 0) { @@ -679,10 +680,12 @@ public class StripePaymentService : IPaymentService Customer = customer.Id, SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items), SubscriptionDefaultTaxRates = subCreateOptions.DefaultTaxRates, + AutomaticTax = new InvoiceAutomaticTaxOptions + { + Enabled = true + } }; - upcomingInvoiceOptions.EnableAutomaticTax(customer, null); - var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions); if (previewInvoice.AmountDue > 0) @@ -801,7 +804,11 @@ public class StripePaymentService : IPaymentService Items = updatedItemOptions, ProrationBehavior = invoiceNow ? Constants.AlwaysInvoice : Constants.CreateProrations, DaysUntilDue = daysUntilDue ?? 1, - CollectionMethod = "send_invoice" + CollectionMethod = "send_invoice", + AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = true + } }; if (!invoiceNow && isAnnualPlan && sub.Status.Trim() != "trialing") { @@ -809,8 +816,6 @@ public class StripePaymentService : IPaymentService new SubscriptionPendingInvoiceItemIntervalOptions { Interval = "month" }; } - subUpdateOptions.EnableAutomaticTax(sub.Customer, sub); - if (!subscriptionUpdate.UpdateNeeded(sub)) { // No need to update subscription, quantity matches @@ -1495,13 +1500,11 @@ public class StripePaymentService : IPaymentService if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId) && customer.Subscriptions.Any(sub => sub.Id == subscriber.GatewaySubscriptionId && - !sub.AutomaticTax.Enabled) && - customer.HasTaxLocationVerified()) + !sub.AutomaticTax.Enabled)) { var subscriptionUpdateOptions = new SubscriptionUpdateOptions { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, - DefaultTaxRates = [] + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } }; _ = await _stripeAdapter.SubscriptionUpdateAsync( From f7d882d760cb8cc1d283d1c1297202f4583f0e09 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Thu, 6 Feb 2025 14:37:15 -0500 Subject: [PATCH 111/148] Remove feature flag from endpoint (#5342) --- src/Api/Auth/Controllers/AccountsController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 7990a5a18a..3f460a3b90 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -974,7 +974,6 @@ public class AccountsController : Controller await _userService.ResendNewDeviceVerificationEmail(request.Email, request.Secret); } - [RequireFeature(FeatureFlagKeys.NewDeviceVerification)] [HttpPost("verify-devices")] [HttpPut("verify-devices")] public async Task SetUserVerifyDevicesAsync([FromBody] SetVerifyDevicesRequestModel request) From 58d2a7ddaaa6d9e78b0ebca5cdfeb8a4ab3f6121 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 6 Feb 2025 21:38:50 +0100 Subject: [PATCH 112/148] [PM-17210] Prevent unintentionally corrupting private keys (#5285) * Prevent unintentionally corrupting private keys * Deny key update only when replacing existing keys * Fix incorrect use of existing user public/encrypted private key * Fix test * Fix tests * Re-add test * Pass through error for set-password * Fix test * Increase test coverage and simplify checks --- .../Auth/Controllers/AccountsController.cs | 12 +- .../Api/Request/Accounts/KeysRequestModel.cs | 24 +++- .../Controllers/AccountsControllerTests.cs | 127 +++++++++++------- 3 files changed, 108 insertions(+), 55 deletions(-) diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 3f460a3b90..1cd9292386 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -266,8 +266,18 @@ public class AccountsController : Controller throw new UnauthorizedAccessException(); } + try + { + user = model.ToUser(user); + } + catch (Exception e) + { + ModelState.AddModelError(string.Empty, e.Message); + throw new BadRequestException(ModelState); + } + var result = await _setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync( - model.ToUser(user), + user, model.MasterPasswordHash, model.Key, model.OrgIdentifier); diff --git a/src/Core/Auth/Models/Api/Request/Accounts/KeysRequestModel.cs b/src/Core/Auth/Models/Api/Request/Accounts/KeysRequestModel.cs index 93832542de..0964fe1a1d 100644 --- a/src/Core/Auth/Models/Api/Request/Accounts/KeysRequestModel.cs +++ b/src/Core/Auth/Models/Api/Request/Accounts/KeysRequestModel.cs @@ -1,26 +1,36 @@ using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; +using Bit.Core.Utilities; namespace Bit.Core.Auth.Models.Api.Request.Accounts; public class KeysRequestModel { + [Required] public string PublicKey { get; set; } [Required] public string EncryptedPrivateKey { get; set; } public User ToUser(User existingUser) { - if (string.IsNullOrWhiteSpace(existingUser.PublicKey) && !string.IsNullOrWhiteSpace(PublicKey)) + if (string.IsNullOrWhiteSpace(PublicKey) || string.IsNullOrWhiteSpace(EncryptedPrivateKey)) + { + throw new InvalidOperationException("Public and private keys are required."); + } + + if (string.IsNullOrWhiteSpace(existingUser.PublicKey) && string.IsNullOrWhiteSpace(existingUser.PrivateKey)) { existingUser.PublicKey = PublicKey; - } - - if (string.IsNullOrWhiteSpace(existingUser.PrivateKey)) - { existingUser.PrivateKey = EncryptedPrivateKey; + return existingUser; + } + else if (PublicKey == existingUser.PublicKey && CoreHelpers.FixedTimeEquals(EncryptedPrivateKey, existingUser.PrivateKey)) + { + return existingUser; + } + else + { + throw new InvalidOperationException("Cannot replace existing key(s) with new key(s)."); } - - return existingUser; } } diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index 1b8c040789..33b7e764d4 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -419,22 +419,32 @@ public class AccountsControllerTests : IDisposable [Theory] - [BitAutoData(true, false)] // User has PublicKey and PrivateKey, and Keys in request are NOT null - [BitAutoData(true, true)] // User has PublicKey and PrivateKey, and Keys in request are null - [BitAutoData(false, false)] // User has neither PublicKey nor PrivateKey, and Keys in request are NOT null - [BitAutoData(false, true)] // User has neither PublicKey nor PrivateKey, and Keys in request are null + [BitAutoData(true, "existingPrivateKey", "existingPublicKey", true)] // allow providing existing keys in the request + [BitAutoData(true, null, null, true)] // allow not setting the public key when the user already has a key + [BitAutoData(false, "newPrivateKey", "newPublicKey", true)] // allow setting new keys when the user has no keys + [BitAutoData(false, null, null, true)] // allow not setting the public key when the user has no keys + // do not allow single key + [BitAutoData(false, "existingPrivateKey", null, false)] + [BitAutoData(false, null, "existingPublicKey", false)] + [BitAutoData(false, "newPrivateKey", null, false)] + [BitAutoData(false, null, "newPublicKey", false)] + [BitAutoData(true, "existingPrivateKey", null, false)] + [BitAutoData(true, null, "existingPublicKey", false)] + [BitAutoData(true, "newPrivateKey", null, false)] + [BitAutoData(true, null, "newPublicKey", false)] + // reject overwriting existing keys + [BitAutoData(true, "newPrivateKey", "newPublicKey", false)] public async Task PostSetPasswordAsync_WhenUserExistsAndSettingPasswordSucceeds_ShouldHandleKeysCorrectlyAndReturn( - bool hasExistingKeys, - bool shouldSetKeysToNull, - User user, - SetPasswordRequestModel setPasswordRequestModel) + bool hasExistingKeys, + string requestPrivateKey, + string requestPublicKey, + bool shouldSucceed, + User user, + SetPasswordRequestModel setPasswordRequestModel) { // Arrange const string existingPublicKey = "existingPublicKey"; - const string existingEncryptedPrivateKey = "existingEncryptedPrivateKey"; - - const string newPublicKey = "newPublicKey"; - const string newEncryptedPrivateKey = "newEncryptedPrivateKey"; + const string existingEncryptedPrivateKey = "existingPrivateKey"; if (hasExistingKeys) { @@ -447,16 +457,16 @@ public class AccountsControllerTests : IDisposable user.PrivateKey = null; } - if (shouldSetKeysToNull) + if (requestPrivateKey == null && requestPublicKey == null) { setPasswordRequestModel.Keys = null; } else { - setPasswordRequestModel.Keys = new KeysRequestModel() + setPasswordRequestModel.Keys = new KeysRequestModel { - PublicKey = newPublicKey, - EncryptedPrivateKey = newEncryptedPrivateKey + EncryptedPrivateKey = requestPrivateKey, + PublicKey = requestPublicKey }; } @@ -469,44 +479,66 @@ public class AccountsControllerTests : IDisposable .Returns(Task.FromResult(IdentityResult.Success)); // Act - await _sut.PostSetPasswordAsync(setPasswordRequestModel); - - // Assert - await _setInitialMasterPasswordCommand.Received(1) - .SetInitialMasterPasswordAsync( - Arg.Is(u => u == user), - Arg.Is(s => s == setPasswordRequestModel.MasterPasswordHash), - Arg.Is(s => s == setPasswordRequestModel.Key), - Arg.Is(s => s == setPasswordRequestModel.OrgIdentifier)); - - // Additional Assertions for User object modifications - Assert.Equal(setPasswordRequestModel.MasterPasswordHint, user.MasterPasswordHint); - Assert.Equal(setPasswordRequestModel.Kdf, user.Kdf); - Assert.Equal(setPasswordRequestModel.KdfIterations, user.KdfIterations); - Assert.Equal(setPasswordRequestModel.KdfMemory, user.KdfMemory); - Assert.Equal(setPasswordRequestModel.KdfParallelism, user.KdfParallelism); - Assert.Equal(setPasswordRequestModel.Key, user.Key); - - if (hasExistingKeys) + if (shouldSucceed) { - // User Keys should not be modified - Assert.Equal(existingPublicKey, user.PublicKey); - Assert.Equal(existingEncryptedPrivateKey, user.PrivateKey); - } - else if (!shouldSetKeysToNull) - { - // User had no keys so they should be set to the request model's keys - Assert.Equal(setPasswordRequestModel.Keys.PublicKey, user.PublicKey); - Assert.Equal(setPasswordRequestModel.Keys.EncryptedPrivateKey, user.PrivateKey); + await _sut.PostSetPasswordAsync(setPasswordRequestModel); + // Assert + await _setInitialMasterPasswordCommand.Received(1) + .SetInitialMasterPasswordAsync( + Arg.Is(u => u == user), + Arg.Is(s => s == setPasswordRequestModel.MasterPasswordHash), + Arg.Is(s => s == setPasswordRequestModel.Key), + Arg.Is(s => s == setPasswordRequestModel.OrgIdentifier)); + + // Additional Assertions for User object modifications + Assert.Equal(setPasswordRequestModel.MasterPasswordHint, user.MasterPasswordHint); + Assert.Equal(setPasswordRequestModel.Kdf, user.Kdf); + Assert.Equal(setPasswordRequestModel.KdfIterations, user.KdfIterations); + Assert.Equal(setPasswordRequestModel.KdfMemory, user.KdfMemory); + Assert.Equal(setPasswordRequestModel.KdfParallelism, user.KdfParallelism); + Assert.Equal(setPasswordRequestModel.Key, user.Key); } else { - // User had no keys and the request model's keys were null, so they should be set to null - Assert.Null(user.PublicKey); - Assert.Null(user.PrivateKey); + await Assert.ThrowsAsync(() => _sut.PostSetPasswordAsync(setPasswordRequestModel)); } } + [Theory] + [BitAutoData] + public async Task PostSetPasswordAsync_WhenUserExistsAndHasKeysAndKeysAreUpdated_ShouldThrowAsync( + User user, + SetPasswordRequestModel setPasswordRequestModel) + { + // Arrange + const string existingPublicKey = "existingPublicKey"; + const string existingEncryptedPrivateKey = "existingEncryptedPrivateKey"; + + const string newPublicKey = "newPublicKey"; + const string newEncryptedPrivateKey = "newEncryptedPrivateKey"; + + user.PublicKey = existingPublicKey; + user.PrivateKey = existingEncryptedPrivateKey; + + setPasswordRequestModel.Keys = new KeysRequestModel() + { + PublicKey = newPublicKey, + EncryptedPrivateKey = newEncryptedPrivateKey + }; + + _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(Task.FromResult(user)); + _setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync( + user, + setPasswordRequestModel.MasterPasswordHash, + setPasswordRequestModel.Key, + setPasswordRequestModel.OrgIdentifier) + .Returns(Task.FromResult(IdentityResult.Success)); + + // Act & Assert + await Assert.ThrowsAsync(() => _sut.PostSetPasswordAsync(setPasswordRequestModel)); + } + + [Theory] [BitAutoData] public async Task PostSetPasswordAsync_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException( @@ -525,6 +557,7 @@ public class AccountsControllerTests : IDisposable User user, SetPasswordRequestModel model) { + model.Keys = null; // Arrange _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(Task.FromResult(user)); _setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) From f8b65e047751b01cfdfc44ea17a029270863707e Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:46:23 -0500 Subject: [PATCH 113/148] Removed all usages of FluentAssertions (#5378) --- .github/renovate.json | 1 - test/Billing.Test/Billing.Test.csproj | 1 - .../Controllers/PayPalControllerTests.cs | 9 +-- .../Services/StripeEventServiceTests.cs | 79 ++++++++----------- 4 files changed, 38 insertions(+), 52 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index affa29bea9..31d78a4d4e 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -64,7 +64,6 @@ "Braintree", "coverlet.collector", "CsvHelper", - "FluentAssertions", "Kralizek.AutoFixture.Extensions.MockHttp", "Microsoft.AspNetCore.Mvc.Testing", "Microsoft.Extensions.Logging", diff --git a/test/Billing.Test/Billing.Test.csproj b/test/Billing.Test/Billing.Test.csproj index 4d71425681..b4ea2938f6 100644 --- a/test/Billing.Test/Billing.Test.csproj +++ b/test/Billing.Test/Billing.Test.csproj @@ -6,7 +6,6 @@ - diff --git a/test/Billing.Test/Controllers/PayPalControllerTests.cs b/test/Billing.Test/Controllers/PayPalControllerTests.cs index 3c9edd2220..a059207c76 100644 --- a/test/Billing.Test/Controllers/PayPalControllerTests.cs +++ b/test/Billing.Test/Controllers/PayPalControllerTests.cs @@ -8,7 +8,6 @@ using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; using Divergic.Logging.Xunit; -using FluentAssertions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -577,14 +576,14 @@ public class PayPalControllerTests { var statusCodeActionResult = (IStatusCodeActionResult)result; - statusCodeActionResult.StatusCode.Should().Be(statusCode); + Assert.Equal(statusCode, statusCodeActionResult.StatusCode); } private static void Logged(ICacheLogger logger, LogLevel logLevel, string message) { - logger.Last.Should().NotBeNull(); - logger.Last!.LogLevel.Should().Be(logLevel); - logger.Last!.Message.Should().Be(message); + Assert.NotNull(logger.Last); + Assert.Equal(logLevel, logger.Last!.LogLevel); + Assert.Equal(message, logger.Last!.Message); } private static void LoggedError(ICacheLogger logger, string message) diff --git a/test/Billing.Test/Services/StripeEventServiceTests.cs b/test/Billing.Test/Services/StripeEventServiceTests.cs index 15aa5c7234..b40e8b9408 100644 --- a/test/Billing.Test/Services/StripeEventServiceTests.cs +++ b/test/Billing.Test/Services/StripeEventServiceTests.cs @@ -2,7 +2,6 @@ using Bit.Billing.Services.Implementations; using Bit.Billing.Test.Utilities; using Bit.Core.Settings; -using FluentAssertions; using Microsoft.Extensions.Logging; using NSubstitute; using Stripe; @@ -36,10 +35,8 @@ public class StripeEventServiceTests var function = async () => await _stripeEventService.GetCharge(stripeEvent); // Assert - await function - .Should() - .ThrowAsync() - .WithMessage($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Charge)}'"); + var exception = await Assert.ThrowsAsync(function); + Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Charge)}'", exception.Message); await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge( Arg.Any(), @@ -58,7 +55,7 @@ public class StripeEventServiceTests var charge = await _stripeEventService.GetCharge(stripeEvent); // Assert - charge.Should().BeEquivalentTo(stripeEvent.Data.Object as Charge); + Assert.Equivalent(stripeEvent.Data.Object as Charge, charge, true); await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge( Arg.Any(), @@ -88,8 +85,8 @@ public class StripeEventServiceTests var charge = await _stripeEventService.GetCharge(stripeEvent, true, expand); // Assert - charge.Should().Be(apiCharge); - charge.Should().NotBeSameAs(eventCharge); + Assert.Equal(apiCharge, charge); + Assert.NotSame(eventCharge, charge); await _stripeFacade.Received().GetCharge( apiCharge.Id, @@ -110,10 +107,8 @@ public class StripeEventServiceTests var function = async () => await _stripeEventService.GetCustomer(stripeEvent); // Assert - await function - .Should() - .ThrowAsync() - .WithMessage($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Customer)}'"); + var exception = await Assert.ThrowsAsync(function); + Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Customer)}'", exception.Message); await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer( Arg.Any(), @@ -132,7 +127,7 @@ public class StripeEventServiceTests var customer = await _stripeEventService.GetCustomer(stripeEvent); // Assert - customer.Should().BeEquivalentTo(stripeEvent.Data.Object as Customer); + Assert.Equivalent(stripeEvent.Data.Object as Customer, customer, true); await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer( Arg.Any(), @@ -162,8 +157,8 @@ public class StripeEventServiceTests var customer = await _stripeEventService.GetCustomer(stripeEvent, true, expand); // Assert - customer.Should().Be(apiCustomer); - customer.Should().NotBeSameAs(eventCustomer); + Assert.Equal(apiCustomer, customer); + Assert.NotSame(eventCustomer, customer); await _stripeFacade.Received().GetCustomer( apiCustomer.Id, @@ -184,10 +179,8 @@ public class StripeEventServiceTests var function = async () => await _stripeEventService.GetInvoice(stripeEvent); // Assert - await function - .Should() - .ThrowAsync() - .WithMessage($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Invoice)}'"); + var exception = await Assert.ThrowsAsync(function); + Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Invoice)}'", exception.Message); await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice( Arg.Any(), @@ -206,7 +199,7 @@ public class StripeEventServiceTests var invoice = await _stripeEventService.GetInvoice(stripeEvent); // Assert - invoice.Should().BeEquivalentTo(stripeEvent.Data.Object as Invoice); + Assert.Equivalent(stripeEvent.Data.Object as Invoice, invoice, true); await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice( Arg.Any(), @@ -236,8 +229,8 @@ public class StripeEventServiceTests var invoice = await _stripeEventService.GetInvoice(stripeEvent, true, expand); // Assert - invoice.Should().Be(apiInvoice); - invoice.Should().NotBeSameAs(eventInvoice); + Assert.Equal(apiInvoice, invoice); + Assert.NotSame(eventInvoice, invoice); await _stripeFacade.Received().GetInvoice( apiInvoice.Id, @@ -258,10 +251,8 @@ public class StripeEventServiceTests var function = async () => await _stripeEventService.GetPaymentMethod(stripeEvent); // Assert - await function - .Should() - .ThrowAsync() - .WithMessage($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(PaymentMethod)}'"); + var exception = await Assert.ThrowsAsync(function); + Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(PaymentMethod)}'", exception.Message); await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod( Arg.Any(), @@ -280,7 +271,7 @@ public class StripeEventServiceTests var paymentMethod = await _stripeEventService.GetPaymentMethod(stripeEvent); // Assert - paymentMethod.Should().BeEquivalentTo(stripeEvent.Data.Object as PaymentMethod); + Assert.Equivalent(stripeEvent.Data.Object as PaymentMethod, paymentMethod, true); await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod( Arg.Any(), @@ -310,8 +301,8 @@ public class StripeEventServiceTests var paymentMethod = await _stripeEventService.GetPaymentMethod(stripeEvent, true, expand); // Assert - paymentMethod.Should().Be(apiPaymentMethod); - paymentMethod.Should().NotBeSameAs(eventPaymentMethod); + Assert.Equal(apiPaymentMethod, paymentMethod); + Assert.NotSame(eventPaymentMethod, paymentMethod); await _stripeFacade.Received().GetPaymentMethod( apiPaymentMethod.Id, @@ -332,10 +323,8 @@ public class StripeEventServiceTests var function = async () => await _stripeEventService.GetSubscription(stripeEvent); // Assert - await function - .Should() - .ThrowAsync() - .WithMessage($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Subscription)}'"); + var exception = await Assert.ThrowsAsync(function); + Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Subscription)}'", exception.Message); await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription( Arg.Any(), @@ -354,7 +343,7 @@ public class StripeEventServiceTests var subscription = await _stripeEventService.GetSubscription(stripeEvent); // Assert - subscription.Should().BeEquivalentTo(stripeEvent.Data.Object as Subscription); + Assert.Equivalent(stripeEvent.Data.Object as Subscription, subscription, true); await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription( Arg.Any(), @@ -384,8 +373,8 @@ public class StripeEventServiceTests var subscription = await _stripeEventService.GetSubscription(stripeEvent, true, expand); // Assert - subscription.Should().Be(apiSubscription); - subscription.Should().NotBeSameAs(eventSubscription); + Assert.Equal(apiSubscription, subscription); + Assert.NotSame(eventSubscription, subscription); await _stripeFacade.Received().GetSubscription( apiSubscription.Id, @@ -417,7 +406,7 @@ public class StripeEventServiceTests var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); // Assert - cloudRegionValid.Should().BeTrue(); + Assert.True(cloudRegionValid); await _stripeFacade.Received(1).GetSubscription( subscription.Id, @@ -447,7 +436,7 @@ public class StripeEventServiceTests var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); // Assert - cloudRegionValid.Should().BeTrue(); + Assert.True(cloudRegionValid); await _stripeFacade.Received(1).GetCharge( charge.Id, @@ -475,7 +464,7 @@ public class StripeEventServiceTests var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); // Assert - cloudRegionValid.Should().BeTrue(); + Assert.True(cloudRegionValid); await _stripeFacade.Received(1).GetCustomer( invoice.CustomerId, @@ -505,7 +494,7 @@ public class StripeEventServiceTests var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); // Assert - cloudRegionValid.Should().BeTrue(); + Assert.True(cloudRegionValid); await _stripeFacade.Received(1).GetInvoice( invoice.Id, @@ -535,7 +524,7 @@ public class StripeEventServiceTests var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); // Assert - cloudRegionValid.Should().BeTrue(); + Assert.True(cloudRegionValid); await _stripeFacade.Received(1).GetPaymentMethod( paymentMethod.Id, @@ -561,7 +550,7 @@ public class StripeEventServiceTests var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); // Assert - cloudRegionValid.Should().BeTrue(); + Assert.True(cloudRegionValid); await _stripeFacade.Received(1).GetCustomer( customer.Id, @@ -592,7 +581,7 @@ public class StripeEventServiceTests var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); // Assert - cloudRegionValid.Should().BeFalse(); + Assert.False(cloudRegionValid); await _stripeFacade.Received(1).GetSubscription( subscription.Id, @@ -623,7 +612,7 @@ public class StripeEventServiceTests var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); // Assert - cloudRegionValid.Should().BeTrue(); + Assert.True(cloudRegionValid); await _stripeFacade.Received(1).GetSubscription( subscription.Id, @@ -657,7 +646,7 @@ public class StripeEventServiceTests var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); // Assert - cloudRegionValid.Should().BeTrue(); + Assert.True(cloudRegionValid); await _stripeFacade.Received(1).GetSubscription( subscription.Id, From af07dffa6f4e1be0e3fb94fa84e67e753d38d059 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:07:43 -0500 Subject: [PATCH 114/148] Relax nullable in test projects (#5379) * Relax nullable in test projects * Fix xUnit Warnings * More xUnit fixes --- Directory.Build.props | 5 +++++ .../Controllers/v2/GroupsControllerTests.cs | 2 +- .../Controllers/v2/UsersControllerTests.cs | 2 +- test/Api.IntegrationTest/Api.IntegrationTest.csproj | 1 - .../AdminConsole/Services/OrganizationServiceTests.cs | 2 -- .../Business/Tokenables/OrgUserInviteTokenableTests.cs | 2 +- test/Core.Test/Utilities/CoreHelpersTests.cs | 6 +++--- test/Core.Test/Utilities/EncryptedStringAttributeTests.cs | 2 +- .../Utilities/StrictEmailAddressAttributeTests.cs | 2 +- .../Utilities/StrictEmailAddressListAttributeTests.cs | 2 +- test/Events.IntegrationTest/Events.IntegrationTest.csproj | 7 +++---- test/Icons.Test/Models/IconLinkTests.cs | 2 +- test/Identity.Test/Identity.Test.csproj | 3 +-- .../Infrastructure.Dapper.Test.csproj | 6 ++---- .../Infrastructure.IntegrationTest.csproj | 1 - 15 files changed, 21 insertions(+), 24 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 88b63d156c..71650fc16c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,6 +8,11 @@ Bit.$(MSBuildProjectName) enable false + + true + annotations + + + true + $(WarningsNotAsErrors);CS8604 diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index f32eccfe8c..50e372791f 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -3,6 +3,8 @@ bitwarden-Billing false + + $(WarningsNotAsErrors);CS9113 diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index a2d4660194..e2aaa9aa23 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -3,6 +3,8 @@ false bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + + $(WarningsNotAsErrors);CS1570;CS1574;CS8602;CS9113;CS1998;CS8604 diff --git a/src/Identity/Identity.csproj b/src/Identity/Identity.csproj index cb506d86e9..e9e188b53f 100644 --- a/src/Identity/Identity.csproj +++ b/src/Identity/Identity.csproj @@ -3,6 +3,8 @@ bitwarden-Identity false + + $(WarningsNotAsErrors);CS0162 diff --git a/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj b/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj index 046009ef73..19512670ce 100644 --- a/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj +++ b/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj @@ -1,5 +1,10 @@ + + + $(WarningsNotAsErrors);CS8618;CS4014 + + diff --git a/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj b/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj index 972d0eac0e..06ad2dc19a 100644 --- a/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj +++ b/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj @@ -1,5 +1,10 @@ + + + $(WarningsNotAsErrors);CS0108;CS8632 + + diff --git a/test/Api.Test/Api.Test.csproj b/test/Api.Test/Api.Test.csproj index d6b31ce930..ec22583caf 100644 --- a/test/Api.Test/Api.Test.csproj +++ b/test/Api.Test/Api.Test.csproj @@ -2,6 +2,8 @@ false + + $(WarningsNotAsErrors);CS8620;CS0169 diff --git a/test/Core.Test/Core.Test.csproj b/test/Core.Test/Core.Test.csproj index 4858afe54d..baace97710 100644 --- a/test/Core.Test/Core.Test.csproj +++ b/test/Core.Test/Core.Test.csproj @@ -2,6 +2,8 @@ false Bit.Core.Test + + $(WarningsNotAsErrors);CS4014 diff --git a/test/Identity.Test/Identity.Test.csproj b/test/Identity.Test/Identity.Test.csproj index fc0cf07b63..34010d811b 100644 --- a/test/Identity.Test/Identity.Test.csproj +++ b/test/Identity.Test/Identity.Test.csproj @@ -2,6 +2,8 @@ false + + $(WarningsNotAsErrors);CS0672;CS1998 From 02262476d6b13915b5c2313353d048702e4a84a5 Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Tue, 11 Feb 2025 10:20:06 -0500 Subject: [PATCH 122/148] [PM-17562] Add Azure Service Bus for Distributed Events (#5382) * [PM-17562] Add Azure Service Bus for Distributed Events * Fix failing test * Addressed issues mentioned in SonarQube * Respond to PR feedback * Respond to PR feedback - make webhook opt-in, remove message body from log --- dev/docker-compose.yml | 15 ++++ dev/servicebusemulator_config.json | 38 ++++++++++ .../Services/EventLoggingListenerService.cs | 0 .../AzureServiceBusEventListenerService.cs | 73 +++++++++++++++++++ .../AzureServiceBusEventWriteService.cs | 43 +++++++++++ .../AzureTableStorageEventHandler.cs | 14 ++++ .../RabbitMqEventListenerService.cs | 18 ++--- ...EventHandler.cs => WebhookEventHandler.cs} | 12 +-- src/Core/Settings/GlobalSettings.cs | 26 ++++++- src/Events/Startup.cs | 14 ++-- src/EventsProcessor/Startup.cs | 33 ++++++++- .../Utilities/ServiceCollectionExtensions.cs | 10 ++- ...erTests.cs => WebhookEventHandlerTests.cs} | 18 ++--- 13 files changed, 278 insertions(+), 36 deletions(-) create mode 100644 dev/servicebusemulator_config.json rename src/Core/{ => AdminConsole}/Services/EventLoggingListenerService.cs (100%) create mode 100644 src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs rename src/Core/{ => AdminConsole}/Services/Implementations/RabbitMqEventListenerService.cs (88%) rename src/Core/AdminConsole/Services/Implementations/{HttpPostEventHandler.cs => WebhookEventHandler.cs} (59%) rename test/Core.Test/AdminConsole/Services/{HttpPostEventHandlerTests.cs => WebhookEventHandlerTests.cs} (74%) diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index d23eaefbb0..1bfbe0a9d7 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -109,6 +109,21 @@ services: profiles: - proxy + service-bus: + container_name: service-bus + image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest + pull_policy: always + volumes: + - "./servicebusemulator_config.json:/ServiceBus_Emulator/ConfigFiles/Config.json" + ports: + - "5672:5672" + environment: + SQL_SERVER: mssql + MSSQL_SA_PASSWORD: "${MSSQL_PASSWORD}" + ACCEPT_EULA: "Y" + profiles: + - servicebus + volumes: mssql_dev_data: postgres_dev_data: diff --git a/dev/servicebusemulator_config.json b/dev/servicebusemulator_config.json new file mode 100644 index 0000000000..f0e4279b06 --- /dev/null +++ b/dev/servicebusemulator_config.json @@ -0,0 +1,38 @@ +{ + "UserConfig": { + "Namespaces": [ + { + "Name": "sbemulatorns", + "Queues": [ + { + "Name": "queue.1", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "RequiresDuplicateDetection": false, + "RequiresSession": false + } + } + ], + "Topics": [ + { + "Name": "event-logging", + "Subscriptions": [ + { + "Name": "events-write-subscription" + } + ] + } + ] + } + ], + "Logging": { + "Type": "File" + } + } +} diff --git a/src/Core/Services/EventLoggingListenerService.cs b/src/Core/AdminConsole/Services/EventLoggingListenerService.cs similarity index 100% rename from src/Core/Services/EventLoggingListenerService.cs rename to src/Core/AdminConsole/Services/EventLoggingListenerService.cs diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs new file mode 100644 index 0000000000..5c329ce8ad --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs @@ -0,0 +1,73 @@ +using System.Text.Json; +using Azure.Messaging.ServiceBus; +using Bit.Core.Models.Data; +using Bit.Core.Settings; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Services; + +public class AzureServiceBusEventListenerService : EventLoggingListenerService +{ + private readonly ILogger _logger; + private readonly ServiceBusClient _client; + private readonly ServiceBusProcessor _processor; + + public AzureServiceBusEventListenerService( + IEventMessageHandler handler, + ILogger logger, + GlobalSettings globalSettings, + string subscriptionName) : base(handler) + { + _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); + _processor = _client.CreateProcessor(globalSettings.EventLogging.AzureServiceBus.TopicName, subscriptionName, new ServiceBusProcessorOptions()); + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + _processor.ProcessMessageAsync += async args => + { + try + { + var eventMessage = JsonSerializer.Deserialize(args.Message.Body.ToString()); + + await _handler.HandleEventAsync(eventMessage); + await args.CompleteMessageAsync(args.Message); + } + catch (Exception exception) + { + _logger.LogError( + exception, + "An error occured while processing message: {MessageId}", + args.Message.MessageId + ); + } + }; + + _processor.ProcessErrorAsync += args => + { + _logger.LogError( + args.Exception, + "An error occurred. Entity Path: {EntityPath}, Error Source: {ErrorSource}", + args.EntityPath, + args.ErrorSource + ); + return Task.CompletedTask; + }; + + await _processor.StartProcessingAsync(cancellationToken); + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + await _processor.StopProcessingAsync(cancellationToken); + await base.StopAsync(cancellationToken); + } + + public override void Dispose() + { + _processor.DisposeAsync().GetAwaiter().GetResult(); + _client.DisposeAsync().GetAwaiter().GetResult(); + base.Dispose(); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs new file mode 100644 index 0000000000..ed8f45ed55 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs @@ -0,0 +1,43 @@ +using System.Text.Json; +using Azure.Messaging.ServiceBus; +using Bit.Core.Models.Data; +using Bit.Core.Services; +using Bit.Core.Settings; + +namespace Bit.Core.AdminConsole.Services.Implementations; + +public class AzureServiceBusEventWriteService : IEventWriteService, IAsyncDisposable +{ + private readonly ServiceBusClient _client; + private readonly ServiceBusSender _sender; + + public AzureServiceBusEventWriteService(GlobalSettings globalSettings) + { + _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); + _sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.TopicName); + } + + public async Task CreateAsync(IEvent e) + { + var message = new ServiceBusMessage(JsonSerializer.SerializeToUtf8Bytes(e)) + { + ContentType = "application/json" + }; + + await _sender.SendMessageAsync(message); + } + + public async Task CreateManyAsync(IEnumerable events) + { + foreach (var e in events) + { + await CreateAsync(e); + } + } + + public async ValueTask DisposeAsync() + { + await _sender.DisposeAsync(); + await _client.DisposeAsync(); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs new file mode 100644 index 0000000000..2612ba0487 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs @@ -0,0 +1,14 @@ +using Bit.Core.Models.Data; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Services; + +public class AzureTableStorageEventHandler( + [FromKeyedServices("persistent")] IEventWriteService eventWriteService) + : IEventMessageHandler +{ + public Task HandleEventAsync(EventMessage eventMessage) + { + return eventWriteService.CreateManyAsync(EventTableEntity.IndexEvent(eventMessage)); + } +} diff --git a/src/Core/Services/Implementations/RabbitMqEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs similarity index 88% rename from src/Core/Services/Implementations/RabbitMqEventListenerService.cs rename to src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs index 9360170368..c302497142 100644 --- a/src/Core/Services/Implementations/RabbitMqEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs @@ -38,7 +38,10 @@ public class RabbitMqEventListenerService : EventLoggingListenerService _connection = await _factory.CreateConnectionAsync(cancellationToken); _channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken); - await _channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true); + await _channel.ExchangeDeclareAsync(exchange: _exchangeName, + type: ExchangeType.Fanout, + durable: true, + cancellationToken: cancellationToken); await _channel.QueueDeclareAsync(queue: _queueName, durable: true, exclusive: false, @@ -52,7 +55,7 @@ public class RabbitMqEventListenerService : EventLoggingListenerService await base.StartAsync(cancellationToken); } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + protected override async Task ExecuteAsync(CancellationToken cancellationToken) { var consumer = new AsyncEventingBasicConsumer(_channel); consumer.ReceivedAsync += async (_, eventArgs) => @@ -68,18 +71,13 @@ public class RabbitMqEventListenerService : EventLoggingListenerService } }; - await _channel.BasicConsumeAsync(_queueName, autoAck: true, consumer: consumer, cancellationToken: stoppingToken); - - while (!stoppingToken.IsCancellationRequested) - { - await Task.Delay(1_000, stoppingToken); - } + await _channel.BasicConsumeAsync(_queueName, autoAck: true, consumer: consumer, cancellationToken: cancellationToken); } public override async Task StopAsync(CancellationToken cancellationToken) { - await _channel.CloseAsync(); - await _connection.CloseAsync(); + await _channel.CloseAsync(cancellationToken); + await _connection.CloseAsync(cancellationToken); await base.StopAsync(cancellationToken); } diff --git a/src/Core/AdminConsole/Services/Implementations/HttpPostEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs similarity index 59% rename from src/Core/AdminConsole/Services/Implementations/HttpPostEventHandler.cs rename to src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs index 8aece0c1da..60abc198d8 100644 --- a/src/Core/AdminConsole/Services/Implementations/HttpPostEventHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs @@ -4,25 +4,25 @@ using Bit.Core.Settings; namespace Bit.Core.Services; -public class HttpPostEventHandler : IEventMessageHandler +public class WebhookEventHandler : IEventMessageHandler { private readonly HttpClient _httpClient; - private readonly string _httpPostUrl; + private readonly string _webhookUrl; - public const string HttpClientName = "HttpPostEventHandlerHttpClient"; + public const string HttpClientName = "WebhookEventHandlerHttpClient"; - public HttpPostEventHandler( + public WebhookEventHandler( IHttpClientFactory httpClientFactory, GlobalSettings globalSettings) { _httpClient = httpClientFactory.CreateClient(HttpClientName); - _httpPostUrl = globalSettings.EventLogging.RabbitMq.HttpPostUrl; + _webhookUrl = globalSettings.EventLogging.WebhookUrl; } public async Task HandleEventAsync(EventMessage eventMessage) { var content = JsonContent.Create(eventMessage); - var response = await _httpClient.PostAsync(_httpPostUrl, content); + var response = await _httpClient.PostAsync(_webhookUrl, content); response.EnsureSuccessStatusCode(); } } diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index a63a36c1c0..a1c7a4fac6 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -260,8 +260,31 @@ public class GlobalSettings : IGlobalSettings public class EventLoggingSettings { + public AzureServiceBusSettings AzureServiceBus { get; set; } = new AzureServiceBusSettings(); + public virtual string WebhookUrl { get; set; } public RabbitMqSettings RabbitMq { get; set; } = new RabbitMqSettings(); + public class AzureServiceBusSettings + { + private string _connectionString; + private string _topicName; + + public virtual string EventRepositorySubscriptionName { get; set; } = "events-write-subscription"; + public virtual string WebhookSubscriptionName { get; set; } = "events-webhook-subscription"; + + public string ConnectionString + { + get => _connectionString; + set => _connectionString = value.Trim('"'); + } + + public string TopicName + { + get => _topicName; + set => _topicName = value.Trim('"'); + } + } + public class RabbitMqSettings { private string _hostName; @@ -270,8 +293,7 @@ public class GlobalSettings : IGlobalSettings private string _exchangeName; public virtual string EventRepositoryQueueName { get; set; } = "events-write-queue"; - public virtual string HttpPostQueueName { get; set; } = "events-httpPost-queue"; - public virtual string HttpPostUrl { get; set; } + public virtual string WebhookQueueName { get; set; } = "events-webhook-queue"; public string HostName { diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index b692733a55..431f449708 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -95,20 +95,20 @@ public class Startup new RabbitMqEventListenerService( provider.GetRequiredService(), provider.GetRequiredService>(), - provider.GetRequiredService(), + globalSettings, globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName)); - if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HttpPostUrl)) + if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.WebhookUrl)) { - services.AddSingleton(); - services.AddHttpClient(HttpPostEventHandler.HttpClientName); + services.AddSingleton(); + services.AddHttpClient(WebhookEventHandler.HttpClientName); services.AddSingleton(provider => new RabbitMqEventListenerService( - provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService>(), - provider.GetRequiredService(), - globalSettings.EventLogging.RabbitMq.HttpPostQueueName)); + globalSettings, + globalSettings.EventLogging.RabbitMq.WebhookQueueName)); } } } diff --git a/src/EventsProcessor/Startup.cs b/src/EventsProcessor/Startup.cs index 2f64c0f926..65d1d36e24 100644 --- a/src/EventsProcessor/Startup.cs +++ b/src/EventsProcessor/Startup.cs @@ -1,8 +1,11 @@ using System.Globalization; +using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.SharedWeb.Utilities; using Microsoft.IdentityModel.Logging; +using TableStorageRepos = Bit.Core.Repositories.TableStorage; namespace Bit.EventsProcessor; @@ -24,9 +27,37 @@ public class Startup services.AddOptions(); // Settings - services.AddGlobalSettingsServices(Configuration, Environment); + var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment); // Hosted Services + + // Optional Azure Service Bus Listeners + if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddKeyedSingleton("persistent"); + services.AddSingleton(provider => + new AzureServiceBusEventListenerService( + provider.GetRequiredService(), + provider.GetRequiredService>(), + globalSettings, + globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName)); + + if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.WebhookUrl)) + { + services.AddSingleton(); + services.AddHttpClient(WebhookEventHandler.HttpClientName); + + services.AddSingleton(provider => + new AzureServiceBusEventListenerService( + provider.GetRequiredService(), + provider.GetRequiredService>(), + globalSettings, + globalSettings.EventLogging.AzureServiceBus.WebhookSubscriptionName)); + } + } services.AddHostedService(); } diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 622b3d7f39..192871bffc 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -321,7 +321,15 @@ public static class ServiceCollectionExtensions if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString)) { - services.AddSingleton(); + if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) + { + services.AddSingleton(); + } + else + { + services.AddSingleton(); + } } else if (globalSettings.SelfHosted) { diff --git a/test/Core.Test/AdminConsole/Services/HttpPostEventHandlerTests.cs b/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs similarity index 74% rename from test/Core.Test/AdminConsole/Services/HttpPostEventHandlerTests.cs rename to test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs index 414b1c54be..eab0be88a1 100644 --- a/test/Core.Test/AdminConsole/Services/HttpPostEventHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs @@ -13,14 +13,14 @@ using GlobalSettings = Bit.Core.Settings.GlobalSettings; namespace Bit.Core.Test.Services; [SutProviderCustomize] -public class HttpPostEventHandlerTests +public class WebhookEventHandlerTests { private readonly MockedHttpMessageHandler _handler; private HttpClient _httpClient; - private const string _httpPostUrl = "http://localhost/test/event"; + private const string _webhookUrl = "http://localhost/test/event"; - public HttpPostEventHandlerTests() + public WebhookEventHandlerTests() { _handler = new MockedHttpMessageHandler(); _handler.Fallback @@ -29,15 +29,15 @@ public class HttpPostEventHandlerTests _httpClient = _handler.ToHttpClient(); } - public SutProvider GetSutProvider() + public SutProvider GetSutProvider() { var clientFactory = Substitute.For(); - clientFactory.CreateClient(HttpPostEventHandler.HttpClientName).Returns(_httpClient); + clientFactory.CreateClient(WebhookEventHandler.HttpClientName).Returns(_httpClient); var globalSettings = new GlobalSettings(); - globalSettings.EventLogging.RabbitMq.HttpPostUrl = _httpPostUrl; + globalSettings.EventLogging.WebhookUrl = _webhookUrl; - return new SutProvider() + return new SutProvider() .SetDependency(globalSettings) .SetDependency(clientFactory) .Create(); @@ -51,7 +51,7 @@ public class HttpPostEventHandlerTests await sutProvider.Sut.HandleEventAsync(eventMessage); sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(HttpPostEventHandler.HttpClientName)) + Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) ); Assert.Single(_handler.CapturedRequests); @@ -60,7 +60,7 @@ public class HttpPostEventHandlerTests var returned = await request.Content.ReadFromJsonAsync(); Assert.Equal(HttpMethod.Post, request.Method); - Assert.Equal(_httpPostUrl, request.RequestUri.ToString()); + Assert.Equal(_webhookUrl, request.RequestUri.ToString()); AssertHelper.AssertPropertyEqual(eventMessage, returned, new[] { "IdempotencyId" }); } } From 9c0f9cf43dcc795d3ea9fe7d70386dcab7ca0af5 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 12 Feb 2025 09:00:52 -0500 Subject: [PATCH 123/148] [PM-18221] Update credited user's billing location when purchasing premium subscription (#5393) * Moved user crediting to PremiumUserBillingService * Fix tests --- src/Billing/Controllers/BitPayController.cs | 11 +-- src/Billing/Controllers/PayPalController.cs | 11 ++- .../Services/IPremiumUserBillingService.cs | 2 + .../PremiumUserBillingService.cs | 82 +++++++++++++++++++ .../Controllers/PayPalControllerTests.cs | 11 ++- 5 files changed, 102 insertions(+), 15 deletions(-) diff --git a/src/Billing/Controllers/BitPayController.cs b/src/Billing/Controllers/BitPayController.cs index 4caf57aa20..3747631bd0 100644 --- a/src/Billing/Controllers/BitPayController.cs +++ b/src/Billing/Controllers/BitPayController.cs @@ -1,6 +1,7 @@ using System.Globalization; using Bit.Billing.Models; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; @@ -25,6 +26,7 @@ public class BitPayController : Controller private readonly IMailService _mailService; private readonly IPaymentService _paymentService; private readonly ILogger _logger; + private readonly IPremiumUserBillingService _premiumUserBillingService; public BitPayController( IOptions billingSettings, @@ -35,7 +37,8 @@ public class BitPayController : Controller IProviderRepository providerRepository, IMailService mailService, IPaymentService paymentService, - ILogger logger) + ILogger logger, + IPremiumUserBillingService premiumUserBillingService) { _billingSettings = billingSettings?.Value; _bitPayClient = bitPayClient; @@ -46,6 +49,7 @@ public class BitPayController : Controller _mailService = mailService; _paymentService = paymentService; _logger = logger; + _premiumUserBillingService = premiumUserBillingService; } [HttpPost("ipn")] @@ -145,10 +149,7 @@ public class BitPayController : Controller if (user != null) { billingEmail = user.BillingEmailAddress(); - if (await _paymentService.CreditAccountAsync(user, tx.Amount)) - { - await _userRepository.ReplaceAsync(user); - } + await _premiumUserBillingService.Credit(user, tx.Amount); } } else if (tx.ProviderId.HasValue) diff --git a/src/Billing/Controllers/PayPalController.cs b/src/Billing/Controllers/PayPalController.cs index 2fc8aab4f2..2afde80601 100644 --- a/src/Billing/Controllers/PayPalController.cs +++ b/src/Billing/Controllers/PayPalController.cs @@ -1,6 +1,7 @@ using System.Text; using Bit.Billing.Models; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; @@ -23,6 +24,7 @@ public class PayPalController : Controller private readonly ITransactionRepository _transactionRepository; private readonly IUserRepository _userRepository; private readonly IProviderRepository _providerRepository; + private readonly IPremiumUserBillingService _premiumUserBillingService; public PayPalController( IOptions billingSettings, @@ -32,7 +34,8 @@ public class PayPalController : Controller IPaymentService paymentService, ITransactionRepository transactionRepository, IUserRepository userRepository, - IProviderRepository providerRepository) + IProviderRepository providerRepository, + IPremiumUserBillingService premiumUserBillingService) { _billingSettings = billingSettings?.Value; _logger = logger; @@ -42,6 +45,7 @@ public class PayPalController : Controller _transactionRepository = transactionRepository; _userRepository = userRepository; _providerRepository = providerRepository; + _premiumUserBillingService = premiumUserBillingService; } [HttpPost("ipn")] @@ -257,10 +261,9 @@ public class PayPalController : Controller { var user = await _userRepository.GetByIdAsync(transaction.UserId.Value); - if (await _paymentService.CreditAccountAsync(user, transaction.Amount)) + if (user != null) { - await _userRepository.ReplaceAsync(user); - + await _premiumUserBillingService.Credit(user, transaction.Amount); billingEmail = user.BillingEmailAddress(); } } diff --git a/src/Core/Billing/Services/IPremiumUserBillingService.cs b/src/Core/Billing/Services/IPremiumUserBillingService.cs index 2161b247b9..b3bb580e2d 100644 --- a/src/Core/Billing/Services/IPremiumUserBillingService.cs +++ b/src/Core/Billing/Services/IPremiumUserBillingService.cs @@ -6,6 +6,8 @@ namespace Bit.Core.Billing.Services; public interface IPremiumUserBillingService { + Task Credit(User user, decimal amount); + /// /// Establishes the Stripe entities necessary for a Bitwarden using the provided . /// diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index ed841c9576..6f571950f5 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -27,6 +27,57 @@ public class PremiumUserBillingService( ISubscriberService subscriberService, IUserRepository userRepository) : IPremiumUserBillingService { + public async Task Credit(User user, decimal amount) + { + var customer = await subscriberService.GetCustomer(user); + + // Negative credit represents a balance and all Stripe denomination is in cents. + var credit = (long)amount * -100; + + if (customer == null) + { + var options = new CustomerCreateOptions + { + Balance = credit, + Description = user.Name, + Email = user.Email, + InvoiceSettings = new CustomerInvoiceSettingsOptions + { + CustomFields = + [ + new CustomerInvoiceSettingsCustomFieldOptions + { + Name = user.SubscriberType(), + Value = user.SubscriberName().Length <= 30 + ? user.SubscriberName() + : user.SubscriberName()[..30] + } + ] + }, + Metadata = new Dictionary + { + ["region"] = globalSettings.BaseServiceUri.CloudRegion, + ["userId"] = user.Id.ToString() + } + }; + + customer = await stripeAdapter.CustomerCreateAsync(options); + + user.Gateway = GatewayType.Stripe; + user.GatewayCustomerId = customer.Id; + await userRepository.ReplaceAsync(user); + } + else + { + var options = new CustomerUpdateOptions + { + Balance = customer.Balance + credit + }; + + await stripeAdapter.CustomerUpdateAsync(customer.Id, options); + } + } + public async Task Finalize(PremiumUserSale sale) { var (user, customerSetup, storage) = sale; @@ -37,6 +88,37 @@ public class PremiumUserBillingService( ? await CreateCustomerAsync(user, customerSetup) : await subscriberService.GetCustomerOrThrow(user, new CustomerGetOptions { Expand = expand }); + /* + * If the customer was previously set up with credit, which does not require a billing location, + * we need to update the customer on the fly before we start the subscription. + */ + if (customerSetup is + { + TokenizedPaymentSource.Type: PaymentMethodType.Credit, + TaxInformation: { Country: not null and not "", PostalCode: not null and not "" } + }) + { + var options = new CustomerUpdateOptions + { + 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, + }, + Expand = ["tax"], + Tax = new CustomerTaxOptions + { + ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately + } + }; + + customer = await stripeAdapter.CustomerUpdateAsync(customer.Id, options); + } + var subscription = await CreateSubscriptionAsync(user.Id, customer, storage); switch (customerSetup.TokenizedPaymentSource) diff --git a/test/Billing.Test/Controllers/PayPalControllerTests.cs b/test/Billing.Test/Controllers/PayPalControllerTests.cs index a059207c76..7ec17bd85a 100644 --- a/test/Billing.Test/Controllers/PayPalControllerTests.cs +++ b/test/Billing.Test/Controllers/PayPalControllerTests.cs @@ -3,6 +3,7 @@ using Bit.Billing.Controllers; using Bit.Billing.Test.Utilities; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; @@ -33,6 +34,7 @@ public class PayPalControllerTests private readonly ITransactionRepository _transactionRepository = Substitute.For(); private readonly IUserRepository _userRepository = Substitute.For(); private readonly IProviderRepository _providerRepository = Substitute.For(); + private readonly IPremiumUserBillingService _premiumUserBillingService = Substitute.For(); private const string _defaultWebhookKey = "webhook-key"; @@ -385,8 +387,6 @@ public class PayPalControllerTests _userRepository.GetByIdAsync(userId).Returns(user); - _paymentService.CreditAccountAsync(user, 48M).Returns(true); - var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody); var result = await controller.PostIpn(); @@ -398,9 +398,7 @@ public class PayPalControllerTests transaction.UserId == userId && transaction.Amount == 48M)); - await _paymentService.Received(1).CreditAccountAsync(user, 48M); - - await _userRepository.Received(1).ReplaceAsync(user); + await _premiumUserBillingService.Received(1).Credit(user, 48M); await _mailService.Received(1).SendAddedCreditAsync(billingEmail, 48M); } @@ -544,7 +542,8 @@ public class PayPalControllerTests _paymentService, _transactionRepository, _userRepository, - _providerRepository); + _providerRepository, + _premiumUserBillingService); var httpContext = new DefaultHttpContext(); From 9f5134e070bbcc0e945874f5d7e852a481a91d88 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:21:12 -0500 Subject: [PATCH 124/148] [PM-3503] Feature flag: Mobile AnonAddy self host alias generation (#5387) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index e41cf62024..a025bbb142 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -174,6 +174,7 @@ public static class FeatureFlagKeys public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync"; public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal"; public const string AndroidMutualTls = "mutual-tls"; + public const string PM3503_MobileAnonAddySelfHostAlias = "anon-addy-self-host-alias"; public static List GetAllKeys() { From ae9bb427a16ff2ff5d1ba3b62101ad7e55bec02e Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Wed, 12 Feb 2025 16:46:30 +0100 Subject: [PATCH 125/148] [PM-10600] Push notification creation to affected clients (#4923) * PM-10600: Notification push notification * PM-10600: Sending to specific client types for relay push notifications * PM-10600: Sending to specific client types for other clients * PM-10600: Send push notification on notification creation * PM-10600: Explicit group names * PM-10600: Id typos * PM-10600: Revert global push notifications * PM-10600: Added DeviceType claim * PM-10600: Sent to organization typo * PM-10600: UT coverage * PM-10600: Small refactor, UTs coverage * PM-10600: UTs coverage * PM-10600: Startup fix * PM-10600: Test fix * PM-10600: Required attribute, organization group for push notification fix * PM-10600: UT coverage * PM-10600: Fix Mobile devices not registering to organization push notifications We only register devices for organization push notifications when the organization is being created. This does not work, since we have a use case (Notification Center) of delivering notifications to all users of organization. This fixes it, by adding the organization id tag when device registers for push notifications. * PM-10600: Unit Test coverage for NotificationHubPushRegistrationService Fixed IFeatureService substitute mocking for Android tests. Added user part of organization test with organizationId tags expectation. * PM-10600: Unit Tests fix to NotificationHubPushRegistrationService after merge conflict * PM-10600: Organization push notifications not sending to mobile device from self-hosted. Self-hosted instance uses relay to register the mobile device against Bitwarden Cloud Api. Only the self-hosted server knows client's organization membership, which means it needs to pass in the organization id's information to the relay. Similarly, for Bitwarden Cloud, the organizaton id will come directly from the server. * PM-10600: Fix self-hosted organization notification not being received by mobile device. When mobile device registers on self-hosted through the relay, every single id, like user id, device id and now organization id needs to be prefixed with the installation id. This have been missing in the PushController that handles this for organization id. * 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. * PM-10600: Merge conflicts fix * merge conflict fix --- .../Push/Controllers/PushController.cs | 6 +- src/Core/Context/CurrentContext.cs | 5 + src/Core/Enums/PushType.cs | 2 + src/Core/Identity/Claims.cs | 1 + .../Request/PushRegistrationRequestModel.cs | 1 + .../Api/Request/PushSendRequestModel.cs | 18 +- src/Core/Models/PushNotification.cs | 9 + .../Commands/CreateNotificationCommand.cs | 12 +- .../NotificationHub/INotificationHubPool.cs | 2 +- .../NotificationHub/NotificationHubPool.cs | 2 +- .../NotificationHubPushNotificationService.cs | 74 +++-- .../NotificationHubPushRegistrationService.cs | 49 +-- .../AzureQueuePushNotificationService.cs | 43 +-- .../Push/Services/IPushNotificationService.cs | 9 +- .../Push/Services/IPushRegistrationService.cs | 2 +- .../MultiServicePushNotificationService.cs | 35 +- .../Services/NoopPushNotificationService.cs | 7 +- .../Services/NoopPushRegistrationService.cs | 2 +- ...NotificationsApiPushNotificationService.cs | 23 +- .../Services/RelayPushNotificationService.cs | 137 ++++---- .../Services/RelayPushRegistrationService.cs | 5 +- .../Services/Implementations/DeviceService.cs | 13 +- src/Identity/IdentityServer/ApiResources.cs | 1 + .../RequestValidators/BaseRequestValidator.cs | 1 + src/Notifications/HubHelpers.cs | 54 +++- src/Notifications/NotificationsHub.cs | 43 ++- .../Utilities/ServiceCollectionExtensions.cs | 6 +- .../NotificationsControllerTests.cs | 25 +- .../AutoFixture/QueueClientFixtures.cs | 35 ++ .../Api/Request/PushSendRequestModelTests.cs | 94 ++++++ .../Commands/CreateNotificationCommandTest.cs | 4 + ...ficationHubPushNotificationServiceTests.cs | 248 ++++++++++++-- ...ficationHubPushRegistrationServiceTests.cs | 306 ++++++++++++++++-- .../AzureQueuePushNotificationServiceTests.cs | 69 ++-- ...ultiServicePushNotificationServiceTests.cs | 76 +++-- test/Core.Test/Services/DeviceServiceTests.cs | 102 ++++-- .../openid-configuration.json | 1 + 37 files changed, 1187 insertions(+), 335 deletions(-) create mode 100644 test/Core.Test/AutoFixture/QueueClientFixtures.cs create mode 100644 test/Core.Test/Models/Api/Request/PushSendRequestModelTests.cs diff --git a/src/Api/Platform/Push/Controllers/PushController.cs b/src/Api/Platform/Push/Controllers/PushController.cs index 4b9f1c3e11..8b9e8b52a0 100644 --- a/src/Api/Platform/Push/Controllers/PushController.cs +++ b/src/Api/Platform/Push/Controllers/PushController.cs @@ -43,7 +43,7 @@ public class PushController : Controller { CheckUsage(); await _pushRegistrationService.CreateOrUpdateRegistrationAsync(model.PushToken, Prefix(model.DeviceId), - Prefix(model.UserId), Prefix(model.Identifier), model.Type); + Prefix(model.UserId), Prefix(model.Identifier), model.Type, model.OrganizationIds.Select(Prefix)); } [HttpPost("delete")] @@ -79,12 +79,12 @@ public class PushController : Controller if (!string.IsNullOrWhiteSpace(model.UserId)) { await _pushNotificationService.SendPayloadToUserAsync(Prefix(model.UserId), - model.Type.Value, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId)); + model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType); } else if (!string.IsNullOrWhiteSpace(model.OrganizationId)) { await _pushNotificationService.SendPayloadToOrganizationAsync(Prefix(model.OrganizationId), - model.Type.Value, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId)); + model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType); } } diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs index 2767b5925f..b4a250fe2b 100644 --- a/src/Core/Context/CurrentContext.cs +++ b/src/Core/Context/CurrentContext.cs @@ -169,6 +169,11 @@ public class CurrentContext : ICurrentContext DeviceIdentifier = GetClaimValue(claimsDict, Claims.Device); + if (Enum.TryParse(GetClaimValue(claimsDict, Claims.DeviceType), out DeviceType deviceType)) + { + DeviceType = deviceType; + } + Organizations = GetOrganizations(claimsDict, orgApi); Providers = GetProviders(claimsDict); diff --git a/src/Core/Enums/PushType.cs b/src/Core/Enums/PushType.cs index ee1b59990f..b656e70601 100644 --- a/src/Core/Enums/PushType.cs +++ b/src/Core/Enums/PushType.cs @@ -27,4 +27,6 @@ public enum PushType : byte SyncOrganizations = 17, SyncOrganizationStatusChanged = 18, SyncOrganizationCollectionSettingChanged = 19, + + SyncNotification = 20, } diff --git a/src/Core/Identity/Claims.cs b/src/Core/Identity/Claims.cs index b1223a6e63..65d5eb210a 100644 --- a/src/Core/Identity/Claims.cs +++ b/src/Core/Identity/Claims.cs @@ -6,6 +6,7 @@ public static class Claims public const string SecurityStamp = "sstamp"; public const string Premium = "premium"; public const string Device = "device"; + public const string DeviceType = "devicetype"; public const string OrganizationOwner = "orgowner"; public const string OrganizationAdmin = "orgadmin"; diff --git a/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs b/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs index 580c1c3b60..ee787dd083 100644 --- a/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs +++ b/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs @@ -15,4 +15,5 @@ public class PushRegistrationRequestModel public DeviceType Type { get; set; } [Required] public string Identifier { get; set; } + public IEnumerable OrganizationIds { get; set; } } diff --git a/src/Core/Models/Api/Request/PushSendRequestModel.cs b/src/Core/Models/Api/Request/PushSendRequestModel.cs index b85c8fb555..7247e6d25f 100644 --- a/src/Core/Models/Api/Request/PushSendRequestModel.cs +++ b/src/Core/Models/Api/Request/PushSendRequestModel.cs @@ -1,18 +1,18 @@ -using System.ComponentModel.DataAnnotations; +#nullable enable +using System.ComponentModel.DataAnnotations; using Bit.Core.Enums; namespace Bit.Core.Models.Api; public class PushSendRequestModel : IValidatableObject { - public string UserId { get; set; } - public string OrganizationId { get; set; } - public string DeviceId { get; set; } - public string Identifier { get; set; } - [Required] - public PushType? Type { get; set; } - [Required] - public object Payload { get; set; } + public string? UserId { get; set; } + public string? OrganizationId { get; set; } + public string? DeviceId { get; set; } + public string? Identifier { get; set; } + public required PushType Type { get; set; } + public required object Payload { get; set; } + public ClientType? ClientType { get; set; } public IEnumerable Validate(ValidationContext validationContext) { diff --git a/src/Core/Models/PushNotification.cs b/src/Core/Models/PushNotification.cs index e2247881ea..fd27ced6c5 100644 --- a/src/Core/Models/PushNotification.cs +++ b/src/Core/Models/PushNotification.cs @@ -45,6 +45,15 @@ public class SyncSendPushNotification public DateTime RevisionDate { get; set; } } +public class SyncNotificationPushNotification +{ + public Guid Id { get; set; } + public Guid? UserId { get; set; } + public Guid? OrganizationId { get; set; } + public ClientType ClientType { get; set; } + public DateTime RevisionDate { get; set; } +} + public class AuthRequestPushNotification { public Guid UserId { get; set; } diff --git a/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs b/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs index 4f76950a34..f378a3688a 100644 --- a/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs +++ b/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs @@ -4,6 +4,7 @@ using Bit.Core.NotificationCenter.Authorization; using Bit.Core.NotificationCenter.Commands.Interfaces; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Platform.Push; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; @@ -14,14 +15,17 @@ public class CreateNotificationCommand : ICreateNotificationCommand private readonly ICurrentContext _currentContext; private readonly IAuthorizationService _authorizationService; private readonly INotificationRepository _notificationRepository; + private readonly IPushNotificationService _pushNotificationService; public CreateNotificationCommand(ICurrentContext currentContext, IAuthorizationService authorizationService, - INotificationRepository notificationRepository) + INotificationRepository notificationRepository, + IPushNotificationService pushNotificationService) { _currentContext = currentContext; _authorizationService = authorizationService; _notificationRepository = notificationRepository; + _pushNotificationService = pushNotificationService; } public async Task CreateAsync(Notification notification) @@ -31,6 +35,10 @@ public class CreateNotificationCommand : ICreateNotificationCommand await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notification, NotificationOperations.Create); - return await _notificationRepository.CreateAsync(notification); + var newNotification = await _notificationRepository.CreateAsync(notification); + + await _pushNotificationService.PushSyncNotificationAsync(newNotification); + + return newNotification; } } diff --git a/src/Core/NotificationHub/INotificationHubPool.cs b/src/Core/NotificationHub/INotificationHubPool.cs index 7c383d7b96..18bae98bc6 100644 --- a/src/Core/NotificationHub/INotificationHubPool.cs +++ b/src/Core/NotificationHub/INotificationHubPool.cs @@ -4,6 +4,6 @@ namespace Bit.Core.NotificationHub; public interface INotificationHubPool { - NotificationHubClient ClientFor(Guid comb); + INotificationHubClient ClientFor(Guid comb); INotificationHubProxy AllClients { get; } } diff --git a/src/Core/NotificationHub/NotificationHubPool.cs b/src/Core/NotificationHub/NotificationHubPool.cs index 7448aad5bd..8993ee2b8e 100644 --- a/src/Core/NotificationHub/NotificationHubPool.cs +++ b/src/Core/NotificationHub/NotificationHubPool.cs @@ -43,7 +43,7 @@ public class NotificationHubPool : INotificationHubPool /// /// /// Thrown when no notification hub is found for a given comb. - public NotificationHubClient ClientFor(Guid comb) + public INotificationHubClient ClientFor(Guid comb) { var possibleConnections = _connections.Where(c => c.RegistrationEnabled(comb)).ToArray(); if (possibleConnections.Length == 0) diff --git a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs index d99cbf3fe7..ed44e69218 100644 --- a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs @@ -12,6 +12,7 @@ using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Notification = Bit.Core.NotificationCenter.Entities.Notification; namespace Bit.Core.NotificationHub; @@ -135,11 +136,7 @@ public class NotificationHubPushNotificationService : IPushNotificationService private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false) { - var message = new UserPushNotification - { - UserId = userId, - Date = DateTime.UtcNow - }; + var message = new UserPushNotification { UserId = userId, Date = DateTime.UtcNow }; await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext); } @@ -184,31 +181,54 @@ public class NotificationHubPushNotificationService : IPushNotificationService await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse); } + public async Task PushSyncNotificationAsync(Notification notification) + { + var message = new SyncNotificationPushNotification + { + Id = notification.Id, + UserId = notification.UserId, + OrganizationId = notification.OrganizationId, + ClientType = notification.ClientType, + RevisionDate = notification.RevisionDate + }; + + if (notification.UserId.HasValue) + { + await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotification, message, true, + notification.ClientType); + } + else if (notification.OrganizationId.HasValue) + { + await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotification, message, + true, notification.ClientType); + } + } + private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type) { - var message = new AuthRequestPushNotification - { - Id = authRequest.Id, - UserId = authRequest.UserId - }; + var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId }; await SendPayloadToUserAsync(authRequest.UserId, type, message, true); } - private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext) + private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext, + ClientType? clientType = null) { - await SendPayloadToUserAsync(userId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext)); + await SendPayloadToUserAsync(userId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext), + clientType: clientType); } - private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload, bool excludeCurrentContext) + private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload, + bool excludeCurrentContext, ClientType? clientType = null) { - await SendPayloadToUserAsync(orgId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext)); + await SendPayloadToOrganizationAsync(orgId.ToString(), type, payload, + GetContextIdentifier(excludeCurrentContext), clientType: clientType); } public async Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, - string deviceId = null) + string deviceId = null, ClientType? clientType = null) { - var tag = BuildTag($"template:payload_userId:{SanitizeTagInput(userId)}", identifier); + var tag = BuildTag($"template:payload_userId:{SanitizeTagInput(userId)}", identifier, clientType); await SendPayloadAsync(tag, type, payload); if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) { @@ -217,9 +237,9 @@ public class NotificationHubPushNotificationService : IPushNotificationService } public async Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier, - string deviceId = null) + string deviceId = null, ClientType? clientType = null) { - var tag = BuildTag($"template:payload && organizationId:{SanitizeTagInput(orgId)}", identifier); + var tag = BuildTag($"template:payload && organizationId:{SanitizeTagInput(orgId)}", identifier, clientType); await SendPayloadAsync(tag, type, payload); if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) { @@ -259,18 +279,23 @@ public class NotificationHubPushNotificationService : IPushNotificationService return null; } - var currentContext = _httpContextAccessor?.HttpContext?. - RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext; + var currentContext = + _httpContextAccessor?.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext; return currentContext?.DeviceIdentifier; } - private string BuildTag(string tag, string identifier) + private string BuildTag(string tag, string identifier, ClientType? clientType) { if (!string.IsNullOrWhiteSpace(identifier)) { tag += $" && !deviceIdentifier:{SanitizeTagInput(identifier)}"; } + if (clientType.HasValue && clientType.Value != ClientType.All) + { + tag += $" && clientType:{clientType}"; + } + return $"({tag})"; } @@ -279,8 +304,7 @@ public class NotificationHubPushNotificationService : IPushNotificationService var results = await _notificationHubPool.AllClients.SendTemplateNotificationAsync( new Dictionary { - { "type", ((byte)type).ToString() }, - { "payload", JsonSerializer.Serialize(payload) } + { "type", ((byte)type).ToString() }, { "payload", JsonSerializer.Serialize(payload) } }, tag); if (_enableTracing) @@ -291,7 +315,9 @@ public class NotificationHubPushNotificationService : IPushNotificationService { continue; } - _logger.LogInformation("Azure Notification Hub Tracking ID: {Id} | {Type} push notification with {Success} successes and {Failure} failures with a payload of {@Payload} and result of {@Results}", + + _logger.LogInformation( + "Azure Notification Hub Tracking ID: {Id} | {Type} push notification with {Success} successes and {Failure} failures with a payload of {@Payload} and result of {@Results}", outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results); } } diff --git a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs index 180b2b641b..0c9bbea425 100644 --- a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs @@ -2,36 +2,26 @@ using Bit.Core.Models.Data; using Bit.Core.Platform.Push; using Bit.Core.Repositories; -using Bit.Core.Settings; +using Bit.Core.Utilities; using Microsoft.Azure.NotificationHubs; -using Microsoft.Extensions.Logging; namespace Bit.Core.NotificationHub; public class NotificationHubPushRegistrationService : IPushRegistrationService { private readonly IInstallationDeviceRepository _installationDeviceRepository; - private readonly GlobalSettings _globalSettings; private readonly INotificationHubPool _notificationHubPool; - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; public NotificationHubPushRegistrationService( IInstallationDeviceRepository installationDeviceRepository, - GlobalSettings globalSettings, - INotificationHubPool notificationHubPool, - IServiceProvider serviceProvider, - ILogger logger) + INotificationHubPool notificationHubPool) { _installationDeviceRepository = installationDeviceRepository; - _globalSettings = globalSettings; _notificationHubPool = notificationHubPool; - _serviceProvider = serviceProvider; - _logger = logger; } public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, - string identifier, DeviceType type) + string identifier, DeviceType type, IEnumerable organizationIds) { if (string.IsNullOrWhiteSpace(pushToken)) { @@ -45,16 +35,21 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService Templates = new Dictionary() }; - installation.Tags = new List - { - $"userId:{userId}" - }; + var clientType = DeviceTypes.ToClientType(type); + + installation.Tags = new List { $"userId:{userId}", $"clientType:{clientType}" }; if (!string.IsNullOrWhiteSpace(identifier)) { installation.Tags.Add("deviceIdentifier:" + identifier); } + var organizationIdsList = organizationIds.ToList(); + foreach (var organizationId in organizationIdsList) + { + installation.Tags.Add($"organizationId:{organizationId}"); + } + string payloadTemplate = null, messageTemplate = null, badgeMessageTemplate = null; switch (type) { @@ -84,10 +79,12 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService break; } - BuildInstallationTemplate(installation, "payload", payloadTemplate, userId, identifier); - BuildInstallationTemplate(installation, "message", messageTemplate, userId, identifier); + BuildInstallationTemplate(installation, "payload", payloadTemplate, userId, identifier, clientType, + organizationIdsList); + BuildInstallationTemplate(installation, "message", messageTemplate, userId, identifier, clientType, + organizationIdsList); BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate, - userId, identifier); + userId, identifier, clientType, organizationIdsList); await ClientFor(GetComb(deviceId)).CreateOrUpdateInstallationAsync(installation); if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) @@ -97,7 +94,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } private void BuildInstallationTemplate(Installation installation, string templateId, string templateBody, - string userId, string identifier) + string userId, string identifier, ClientType clientType, List organizationIds) { if (templateBody == null) { @@ -111,8 +108,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService Body = templateBody, Tags = new List { - fullTemplateId, - $"{fullTemplateId}_userId:{userId}" + fullTemplateId, $"{fullTemplateId}_userId:{userId}", $"clientType:{clientType}" } }; @@ -121,6 +117,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService template.Tags.Add($"{fullTemplateId}_deviceIdentifier:{identifier}"); } + foreach (var organizationId in organizationIds) + { + template.Tags.Add($"organizationId:{organizationId}"); + } + installation.Templates.Add(fullTemplateId, template); } @@ -197,7 +198,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } } - private NotificationHubClient ClientFor(Guid deviceId) + private INotificationHubClient ClientFor(Guid deviceId) { return _notificationHubPool.ClientFor(deviceId); } diff --git a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs index 33272ce870..d3509c5437 100644 --- a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs +++ b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs @@ -5,26 +5,25 @@ using Bit.Core.Auth.Entities; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models; -using Bit.Core.Settings; +using Bit.Core.NotificationCenter.Entities; using Bit.Core.Tools.Entities; using Bit.Core.Utilities; using Bit.Core.Vault.Entities; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Platform.Push.Internal; public class AzureQueuePushNotificationService : IPushNotificationService { private readonly QueueClient _queueClient; - private readonly GlobalSettings _globalSettings; private readonly IHttpContextAccessor _httpContextAccessor; public AzureQueuePushNotificationService( - GlobalSettings globalSettings, + [FromKeyedServices("notifications")] QueueClient queueClient, IHttpContextAccessor httpContextAccessor) { - _queueClient = new QueueClient(globalSettings.Notifications.ConnectionString, "notifications"); - _globalSettings = globalSettings; + _queueClient = queueClient; _httpContextAccessor = httpContextAccessor; } @@ -129,11 +128,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false) { - var message = new UserPushNotification - { - UserId = userId, - Date = DateTime.UtcNow - }; + var message = new UserPushNotification { UserId = userId, Date = DateTime.UtcNow }; await SendMessageAsync(type, message, excludeCurrentContext); } @@ -150,11 +145,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type) { - var message = new AuthRequestPushNotification - { - Id = authRequest.Id, - UserId = authRequest.UserId - }; + var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId }; await SendMessageAsync(type, message, true); } @@ -174,6 +165,20 @@ public class AzureQueuePushNotificationService : IPushNotificationService await PushSendAsync(send, PushType.SyncSendDelete); } + public async Task PushSyncNotificationAsync(Notification notification) + { + var message = new SyncNotificationPushNotification + { + Id = notification.Id, + UserId = notification.UserId, + OrganizationId = notification.OrganizationId, + ClientType = notification.ClientType, + RevisionDate = notification.RevisionDate + }; + + await SendMessageAsync(PushType.SyncNotification, message, true); + } + private async Task PushSendAsync(Send send, PushType type) { if (send.UserId.HasValue) @@ -204,20 +209,20 @@ public class AzureQueuePushNotificationService : IPushNotificationService return null; } - var currentContext = _httpContextAccessor?.HttpContext?. - RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext; + var currentContext = + _httpContextAccessor?.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext; return currentContext?.DeviceIdentifier; } public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, - string deviceId = null) + string deviceId = null, ClientType? clientType = null) { // Noop return Task.FromResult(0); } public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier, - string deviceId = null) + string deviceId = null, ClientType? clientType = null) { // Noop return Task.FromResult(0); diff --git a/src/Core/Platform/Push/Services/IPushNotificationService.cs b/src/Core/Platform/Push/Services/IPushNotificationService.cs index b015c17df2..5e1ab7067e 100644 --- a/src/Core/Platform/Push/Services/IPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/IPushNotificationService.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Enums; +using Bit.Core.NotificationCenter.Entities; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; @@ -23,11 +24,13 @@ public interface IPushNotificationService Task PushSyncSendCreateAsync(Send send); Task PushSyncSendUpdateAsync(Send send); Task PushSyncSendDeleteAsync(Send send); + Task PushSyncNotificationAsync(Notification notification); Task PushAuthRequestAsync(AuthRequest authRequest); Task PushAuthRequestResponseAsync(AuthRequest authRequest); - 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); Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization); + Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, + string deviceId = null, ClientType? clientType = null); + Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier, + string deviceId = null, ClientType? clientType = null); } diff --git a/src/Core/Platform/Push/Services/IPushRegistrationService.cs b/src/Core/Platform/Push/Services/IPushRegistrationService.cs index 482e7ae1c4..0c4271f061 100644 --- a/src/Core/Platform/Push/Services/IPushRegistrationService.cs +++ b/src/Core/Platform/Push/Services/IPushRegistrationService.cs @@ -5,7 +5,7 @@ namespace Bit.Core.Platform.Push; public interface IPushRegistrationService { Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, - string identifier, DeviceType type); + string identifier, DeviceType type, IEnumerable organizationIds); Task DeleteRegistrationAsync(string deviceId); Task AddUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId); Task DeleteUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId); diff --git a/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs b/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs index f1a5700013..4ad81e223b 100644 --- a/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs +++ b/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Enums; +using Bit.Core.NotificationCenter.Entities; using Bit.Core.Settings; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; @@ -131,20 +132,6 @@ public class MultiServicePushNotificationService : IPushNotificationService return Task.FromResult(0); } - public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, - string deviceId = null) - { - PushToServices((s) => s.SendPayloadToUserAsync(userId, type, payload, identifier, deviceId)); - return Task.FromResult(0); - } - - public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier, - string deviceId = null) - { - PushToServices((s) => s.SendPayloadToOrganizationAsync(orgId, type, payload, identifier, deviceId)); - return Task.FromResult(0); - } - public Task PushSyncOrganizationStatusAsync(Organization organization) { PushToServices((s) => s.PushSyncOrganizationStatusAsync(organization)); @@ -157,6 +144,26 @@ public class MultiServicePushNotificationService : IPushNotificationService return Task.CompletedTask; } + public Task PushSyncNotificationAsync(Notification notification) + { + PushToServices((s) => s.PushSyncNotificationAsync(notification)); + return Task.CompletedTask; + } + + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, + string deviceId = null, ClientType? clientType = null) + { + PushToServices((s) => s.SendPayloadToUserAsync(userId, type, payload, identifier, deviceId, clientType)); + return Task.FromResult(0); + } + + public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier, + string deviceId = null, ClientType? clientType = null) + { + PushToServices((s) => s.SendPayloadToOrganizationAsync(orgId, type, payload, identifier, deviceId, clientType)); + return Task.FromResult(0); + } + private void PushToServices(Func pushFunc) { if (_services != null) diff --git a/src/Core/Platform/Push/Services/NoopPushNotificationService.cs b/src/Core/Platform/Push/Services/NoopPushNotificationService.cs index 4a185bee1a..463a2fde88 100644 --- a/src/Core/Platform/Push/Services/NoopPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/NoopPushNotificationService.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Enums; +using Bit.Core.NotificationCenter.Entities; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; @@ -84,7 +85,7 @@ public class NoopPushNotificationService : IPushNotificationService } public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier, - string deviceId = null) + string deviceId = null, ClientType? clientType = null) { return Task.FromResult(0); } @@ -107,8 +108,10 @@ public class NoopPushNotificationService : IPushNotificationService } public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, - string deviceId = null) + string deviceId = null, ClientType? clientType = null) { return Task.FromResult(0); } + + public Task PushSyncNotificationAsync(Notification notification) => Task.CompletedTask; } diff --git a/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs b/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs index 6d1716a6ce..6bcf9e893a 100644 --- a/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs +++ b/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs @@ -10,7 +10,7 @@ public class NoopPushRegistrationService : IPushRegistrationService } public Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, - string identifier, DeviceType type) + string identifier, DeviceType type, IEnumerable organizationIds) { return Task.FromResult(0); } diff --git a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs index 5ebfc811ef..5c6b46f63e 100644 --- a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs @@ -3,6 +3,7 @@ using Bit.Core.Auth.Entities; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models; +using Bit.Core.NotificationCenter.Entities; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tools.Entities; @@ -183,6 +184,20 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService await PushSendAsync(send, PushType.SyncSendDelete); } + public async Task PushSyncNotificationAsync(Notification notification) + { + var message = new SyncNotificationPushNotification + { + Id = notification.Id, + UserId = notification.UserId, + OrganizationId = notification.OrganizationId, + ClientType = notification.ClientType, + RevisionDate = notification.RevisionDate + }; + + await SendMessageAsync(PushType.SyncNotification, message, true); + } + private async Task PushSendAsync(Send send, PushType type) { if (send.UserId.HasValue) @@ -212,20 +227,20 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService return null; } - var currentContext = _httpContextAccessor?.HttpContext?. - RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext; + var currentContext = + _httpContextAccessor?.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext; return currentContext?.DeviceIdentifier; } public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, - string deviceId = null) + string deviceId = null, ClientType? clientType = null) { // Noop return Task.FromResult(0); } public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier, - string deviceId = null) + string deviceId = null, ClientType? clientType = null) { // Noop return Task.FromResult(0); diff --git a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs index 6549ab47c3..f51ab004a6 100644 --- a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs @@ -5,6 +5,7 @@ using Bit.Core.Enums; using Bit.Core.IdentityServer; using Bit.Core.Models; using Bit.Core.Models.Api; +using Bit.Core.NotificationCenter.Entities; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -138,11 +139,7 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false) { - var message = new UserPushNotification - { - UserId = userId, - Date = DateTime.UtcNow - }; + var message = new UserPushNotification { UserId = userId, Date = DateTime.UtcNow }; await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext); } @@ -189,69 +186,32 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type) { - var message = new AuthRequestPushNotification - { - Id = authRequest.Id, - UserId = authRequest.UserId - }; + var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId }; await SendPayloadToUserAsync(authRequest.UserId, type, message, true); } - private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext) + public async Task PushSyncNotificationAsync(Notification notification) { - var request = new PushSendRequestModel + var message = new SyncNotificationPushNotification { - UserId = userId.ToString(), - Type = type, - Payload = payload + Id = notification.Id, + UserId = notification.UserId, + OrganizationId = notification.OrganizationId, + ClientType = notification.ClientType, + RevisionDate = notification.RevisionDate }; - await AddCurrentContextAsync(request, excludeCurrentContext); - await SendAsync(HttpMethod.Post, "push/send", request); - } - - private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload, bool excludeCurrentContext) - { - var request = new PushSendRequestModel + if (notification.UserId.HasValue) { - OrganizationId = orgId.ToString(), - Type = type, - Payload = payload - }; - - await AddCurrentContextAsync(request, excludeCurrentContext); - await SendAsync(HttpMethod.Post, "push/send", request); - } - - private async Task AddCurrentContextAsync(PushSendRequestModel request, bool addIdentifier) - { - var currentContext = _httpContextAccessor?.HttpContext?. - RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext; - if (!string.IsNullOrWhiteSpace(currentContext?.DeviceIdentifier)) - { - var device = await _deviceRepository.GetByIdentifierAsync(currentContext.DeviceIdentifier); - if (device != null) - { - request.DeviceId = device.Id.ToString(); - } - if (addIdentifier) - { - request.Identifier = currentContext.DeviceIdentifier; - } + await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotification, message, true, + notification.ClientType); + } + else if (notification.OrganizationId.HasValue) + { + await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotification, message, + true, notification.ClientType); } - } - - public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, - string deviceId = null) - { - throw new NotImplementedException(); - } - - public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier, - string deviceId = null) - { - throw new NotImplementedException(); } public async Task PushSyncOrganizationStatusAsync(Organization organization) @@ -278,4 +238,65 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti }, false ); + + private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext, + ClientType? clientType = null) + { + var request = new PushSendRequestModel + { + UserId = userId.ToString(), + Type = type, + Payload = payload, + ClientType = clientType + }; + + await AddCurrentContextAsync(request, excludeCurrentContext); + await SendAsync(HttpMethod.Post, "push/send", request); + } + + private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload, + bool excludeCurrentContext, ClientType? clientType = null) + { + var request = new PushSendRequestModel + { + OrganizationId = orgId.ToString(), + Type = type, + Payload = payload, + ClientType = clientType + }; + + await AddCurrentContextAsync(request, excludeCurrentContext); + await SendAsync(HttpMethod.Post, "push/send", request); + } + + private async Task AddCurrentContextAsync(PushSendRequestModel request, bool addIdentifier) + { + var currentContext = + _httpContextAccessor?.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext; + if (!string.IsNullOrWhiteSpace(currentContext?.DeviceIdentifier)) + { + var device = await _deviceRepository.GetByIdentifierAsync(currentContext.DeviceIdentifier); + if (device != null) + { + request.DeviceId = device.Id.ToString(); + } + + if (addIdentifier) + { + request.Identifier = currentContext.DeviceIdentifier; + } + } + } + + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, + string deviceId = null, ClientType? clientType = null) + { + throw new NotImplementedException(); + } + + public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier, + string deviceId = null, ClientType? clientType = null) + { + throw new NotImplementedException(); + } } diff --git a/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs b/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs index 79b033e877..b838fbde59 100644 --- a/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs +++ b/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs @@ -25,7 +25,7 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi } public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, - string identifier, DeviceType type) + string identifier, DeviceType type, IEnumerable organizationIds) { var requestModel = new PushRegistrationRequestModel { @@ -33,7 +33,8 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi Identifier = identifier, PushToken = pushToken, Type = type, - UserId = userId + UserId = userId, + OrganizationIds = organizationIds }; await SendAsync(HttpMethod.Post, "push/register", requestModel); } diff --git a/src/Core/Services/Implementations/DeviceService.cs b/src/Core/Services/Implementations/DeviceService.cs index afbc574417..28823eeda7 100644 --- a/src/Core/Services/Implementations/DeviceService.cs +++ b/src/Core/Services/Implementations/DeviceService.cs @@ -1,6 +1,7 @@ using Bit.Core.Auth.Models.Api.Request; using Bit.Core.Auth.Utilities; using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Platform.Push; using Bit.Core.Repositories; @@ -11,13 +12,16 @@ public class DeviceService : IDeviceService { private readonly IDeviceRepository _deviceRepository; private readonly IPushRegistrationService _pushRegistrationService; + private readonly IOrganizationUserRepository _organizationUserRepository; public DeviceService( IDeviceRepository deviceRepository, - IPushRegistrationService pushRegistrationService) + IPushRegistrationService pushRegistrationService, + IOrganizationUserRepository organizationUserRepository) { _deviceRepository = deviceRepository; _pushRegistrationService = pushRegistrationService; + _organizationUserRepository = organizationUserRepository; } public async Task SaveAsync(Device device) @@ -32,8 +36,13 @@ public class DeviceService : IDeviceService await _deviceRepository.ReplaceAsync(device); } + var organizationIdsString = + (await _organizationUserRepository.GetManyDetailsByUserAsync(device.UserId, + OrganizationUserStatusType.Confirmed)) + .Select(ou => ou.OrganizationId.ToString()); + await _pushRegistrationService.CreateOrUpdateRegistrationAsync(device.PushToken, device.Id.ToString(), - device.UserId.ToString(), device.Identifier, device.Type); + device.UserId.ToString(), device.Identifier, device.Type, organizationIdsString); } public async Task ClearTokenAsync(Device device) diff --git a/src/Identity/IdentityServer/ApiResources.cs b/src/Identity/IdentityServer/ApiResources.cs index a0712aafe7..f969d67908 100644 --- a/src/Identity/IdentityServer/ApiResources.cs +++ b/src/Identity/IdentityServer/ApiResources.cs @@ -18,6 +18,7 @@ public class ApiResources Claims.SecurityStamp, Claims.Premium, Claims.Device, + Claims.DeviceType, Claims.OrganizationOwner, Claims.OrganizationAdmin, Claims.OrganizationUser, diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index ea207a7aaa..5e78212cf1 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -210,6 +210,7 @@ public abstract class BaseRequestValidator where T : class if (device != null) { claims.Add(new Claim(Claims.Device, device.Identifier)); + claims.Add(new Claim(Claims.DeviceType, device.Type.ToString())); } var customResponse = new Dictionary(); diff --git a/src/Notifications/HubHelpers.cs b/src/Notifications/HubHelpers.cs index 6f49822dc9..6d17ca9955 100644 --- a/src/Notifications/HubHelpers.cs +++ b/src/Notifications/HubHelpers.cs @@ -10,6 +10,8 @@ public static class HubHelpers private static JsonSerializerOptions _deserializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + private static readonly string _receiveMessageMethod = "ReceiveMessage"; + public static async Task SendNotificationToHubAsync( string notificationJson, IHubContext hubContext, @@ -18,7 +20,8 @@ public static class HubHelpers CancellationToken cancellationToken = default(CancellationToken) ) { - var notification = JsonSerializer.Deserialize>(notificationJson, _deserializerOptions); + var notification = + JsonSerializer.Deserialize>(notificationJson, _deserializerOptions); logger.LogInformation("Sending notification: {NotificationType}", notification.Type); switch (notification.Type) { @@ -32,14 +35,15 @@ public static class HubHelpers if (cipherNotification.Payload.UserId.HasValue) { await hubContext.Clients.User(cipherNotification.Payload.UserId.ToString()) - .SendAsync("ReceiveMessage", cipherNotification, cancellationToken); + .SendAsync(_receiveMessageMethod, cipherNotification, cancellationToken); } else if (cipherNotification.Payload.OrganizationId.HasValue) { - await hubContext.Clients.Group( - $"Organization_{cipherNotification.Payload.OrganizationId}") - .SendAsync("ReceiveMessage", cipherNotification, cancellationToken); + await hubContext.Clients + .Group(NotificationsHub.GetOrganizationGroup(cipherNotification.Payload.OrganizationId.Value)) + .SendAsync(_receiveMessageMethod, cipherNotification, cancellationToken); } + break; case PushType.SyncFolderUpdate: case PushType.SyncFolderCreate: @@ -48,7 +52,7 @@ public static class HubHelpers JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); await hubContext.Clients.User(folderNotification.Payload.UserId.ToString()) - .SendAsync("ReceiveMessage", folderNotification, cancellationToken); + .SendAsync(_receiveMessageMethod, folderNotification, cancellationToken); break; case PushType.SyncCiphers: case PushType.SyncVault: @@ -60,30 +64,30 @@ public static class HubHelpers JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); await hubContext.Clients.User(userNotification.Payload.UserId.ToString()) - .SendAsync("ReceiveMessage", userNotification, cancellationToken); + .SendAsync(_receiveMessageMethod, userNotification, cancellationToken); break; case PushType.SyncSendCreate: case PushType.SyncSendUpdate: case PushType.SyncSendDelete: var sendNotification = JsonSerializer.Deserialize>( - notificationJson, _deserializerOptions); + notificationJson, _deserializerOptions); await hubContext.Clients.User(sendNotification.Payload.UserId.ToString()) - .SendAsync("ReceiveMessage", sendNotification, cancellationToken); + .SendAsync(_receiveMessageMethod, sendNotification, cancellationToken); break; case PushType.AuthRequestResponse: var authRequestResponseNotification = JsonSerializer.Deserialize>( - notificationJson, _deserializerOptions); + notificationJson, _deserializerOptions); await anonymousHubContext.Clients.Group(authRequestResponseNotification.Payload.Id.ToString()) .SendAsync("AuthRequestResponseRecieved", authRequestResponseNotification, cancellationToken); break; case PushType.AuthRequest: var authRequestNotification = JsonSerializer.Deserialize>( - notificationJson, _deserializerOptions); + notificationJson, _deserializerOptions); await hubContext.Clients.User(authRequestNotification.Payload.UserId.ToString()) - .SendAsync("ReceiveMessage", authRequestNotification, cancellationToken); + .SendAsync(_receiveMessageMethod, authRequestNotification, cancellationToken); break; case PushType.SyncOrganizationStatusChanged: var orgStatusNotification = @@ -99,6 +103,32 @@ public static class HubHelpers await hubContext.Clients.Group($"Organization_{organizationCollectionSettingsChangedNotification.Payload.OrganizationId}") .SendAsync("ReceiveMessage", organizationCollectionSettingsChangedNotification, cancellationToken); break; + case PushType.SyncNotification: + var syncNotification = + JsonSerializer.Deserialize>( + notificationJson, _deserializerOptions); + if (syncNotification.Payload.UserId.HasValue) + { + if (syncNotification.Payload.ClientType == ClientType.All) + { + await hubContext.Clients.User(syncNotification.Payload.UserId.ToString()) + .SendAsync(_receiveMessageMethod, syncNotification, cancellationToken); + } + else + { + await hubContext.Clients.Group(NotificationsHub.GetUserGroup( + syncNotification.Payload.UserId.Value, syncNotification.Payload.ClientType)) + .SendAsync(_receiveMessageMethod, syncNotification, cancellationToken); + } + } + else if (syncNotification.Payload.OrganizationId.HasValue) + { + await hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup( + syncNotification.Payload.OrganizationId.Value, syncNotification.Payload.ClientType)) + .SendAsync(_receiveMessageMethod, syncNotification, cancellationToken); + } + + break; default: break; } diff --git a/src/Notifications/NotificationsHub.cs b/src/Notifications/NotificationsHub.cs index a86cf329c5..27cd19c0a0 100644 --- a/src/Notifications/NotificationsHub.cs +++ b/src/Notifications/NotificationsHub.cs @@ -1,5 +1,7 @@ using Bit.Core.Context; +using Bit.Core.Enums; using Bit.Core.Settings; +using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; namespace Bit.Notifications; @@ -20,13 +22,25 @@ public class NotificationsHub : Microsoft.AspNetCore.SignalR.Hub { var currentContext = new CurrentContext(null, null); await currentContext.BuildAsync(Context.User, _globalSettings); + + var clientType = DeviceTypes.ToClientType(currentContext.DeviceType); + if (clientType != ClientType.All && currentContext.UserId.HasValue) + { + await Groups.AddToGroupAsync(Context.ConnectionId, GetUserGroup(currentContext.UserId.Value, clientType)); + } + if (currentContext.Organizations != null) { foreach (var org in currentContext.Organizations) { - await Groups.AddToGroupAsync(Context.ConnectionId, $"Organization_{org.Id}"); + await Groups.AddToGroupAsync(Context.ConnectionId, GetOrganizationGroup(org.Id)); + if (clientType != ClientType.All) + { + await Groups.AddToGroupAsync(Context.ConnectionId, GetOrganizationGroup(org.Id, clientType)); + } } } + _connectionCounter.Increment(); await base.OnConnectedAsync(); } @@ -35,14 +49,39 @@ public class NotificationsHub : Microsoft.AspNetCore.SignalR.Hub { var currentContext = new CurrentContext(null, null); await currentContext.BuildAsync(Context.User, _globalSettings); + + var clientType = DeviceTypes.ToClientType(currentContext.DeviceType); + if (clientType != ClientType.All && currentContext.UserId.HasValue) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, + GetUserGroup(currentContext.UserId.Value, clientType)); + } + if (currentContext.Organizations != null) { foreach (var org in currentContext.Organizations) { - await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"Organization_{org.Id}"); + await Groups.RemoveFromGroupAsync(Context.ConnectionId, GetOrganizationGroup(org.Id)); + if (clientType != ClientType.All) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, GetOrganizationGroup(org.Id, clientType)); + } } } + _connectionCounter.Decrement(); await base.OnDisconnectedAsync(exception); } + + public static string GetUserGroup(Guid userId, ClientType clientType) + { + return $"UserClientType_{userId}_{clientType}"; + } + + public static string GetOrganizationGroup(Guid organizationId, ClientType? clientType = null) + { + return clientType is null or ClientType.All + ? $"Organization_{organizationId}" + : $"OrganizationClientType_{organizationId}_{clientType}"; + } } diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 192871bffc..5a1205c961 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using System.Reflection; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using AspNetCoreRateLimit; +using Azure.Storage.Queues; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Services; @@ -306,7 +307,10 @@ public static class ServiceCollectionExtensions services.AddKeyedSingleton("implementation"); if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString)) { - services.AddKeyedSingleton("implementation"); + services.AddKeyedSingleton("notifications", + (_, _) => new QueueClient(globalSettings.Notifications.ConnectionString, "notifications")); + services.AddKeyedSingleton( + "implementation"); } } 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(); } } diff --git a/test/Core.Test/AutoFixture/QueueClientFixtures.cs b/test/Core.Test/AutoFixture/QueueClientFixtures.cs new file mode 100644 index 0000000000..2a722f3853 --- /dev/null +++ b/test/Core.Test/AutoFixture/QueueClientFixtures.cs @@ -0,0 +1,35 @@ +#nullable enable +using AutoFixture; +using AutoFixture.Kernel; +using Azure.Storage.Queues; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; + +namespace Bit.Core.Test.AutoFixture; + +public class QueueClientBuilder : ISpecimenBuilder +{ + public object Create(object request, ISpecimenContext context) + { + var type = request as Type; + if (type == typeof(QueueClient)) + { + return Substitute.For(); + } + + return new NoSpecimen(); + } +} + +public class QueueClientCustomizeAttribute : BitCustomizeAttribute +{ + public override ICustomization GetCustomization() => new QueueClientFixtures(); +} + +public class QueueClientFixtures : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customizations.Add(new QueueClientBuilder()); + } +} diff --git a/test/Core.Test/Models/Api/Request/PushSendRequestModelTests.cs b/test/Core.Test/Models/Api/Request/PushSendRequestModelTests.cs new file mode 100644 index 0000000000..41a6c25bf2 --- /dev/null +++ b/test/Core.Test/Models/Api/Request/PushSendRequestModelTests.cs @@ -0,0 +1,94 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using Bit.Core.Enums; +using Bit.Core.Models.Api; +using Bit.Core.Utilities; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.Models.Api.Request; + +public class PushSendRequestModelTests +{ + [Theory] + [InlineData(null, null)] + [InlineData(null, "")] + [InlineData(null, " ")] + [InlineData("", null)] + [InlineData(" ", null)] + [InlineData("", "")] + [InlineData(" ", " ")] + public void Validate_UserIdOrganizationIdNullOrEmpty_Invalid(string? userId, string? organizationId) + { + var model = new PushSendRequestModel + { + UserId = userId, + OrganizationId = organizationId, + Type = PushType.SyncCiphers, + Payload = "test" + }; + + var results = Validate(model); + + Assert.Single(results); + Assert.Contains(results, result => result.ErrorMessage == "UserId or OrganizationId is required."); + } + + [Theory] + [BitAutoData("Payload")] + [BitAutoData("Type")] + public void Validate_RequiredFieldNotProvided_Invalid(string requiredField) + { + var model = new PushSendRequestModel + { + UserId = Guid.NewGuid().ToString(), + OrganizationId = Guid.NewGuid().ToString(), + Type = PushType.SyncCiphers, + Payload = "test" + }; + + var dictionary = new Dictionary(); + foreach (var property in model.GetType().GetProperties()) + { + if (property.Name == requiredField) + { + continue; + } + + dictionary[property.Name] = property.GetValue(model); + } + + var serialized = JsonSerializer.Serialize(dictionary, JsonHelpers.IgnoreWritingNull); + var jsonException = + Assert.Throws(() => JsonSerializer.Deserialize(serialized)); + Assert.Contains($"missing required properties, including the following: {requiredField}", + jsonException.Message); + } + + [Fact] + public void Validate_AllFieldsPresent_Valid() + { + var model = new PushSendRequestModel + { + UserId = Guid.NewGuid().ToString(), + OrganizationId = Guid.NewGuid().ToString(), + Type = PushType.SyncCiphers, + Payload = "test payload", + Identifier = Guid.NewGuid().ToString(), + ClientType = ClientType.All, + DeviceId = Guid.NewGuid().ToString() + }; + + var results = Validate(model); + + Assert.Empty(results); + } + + private static List Validate(PushSendRequestModel model) + { + var results = new List(); + Validator.TryValidateObject(model, new ValidationContext(model), results, true); + return results; + } +} diff --git a/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs index 4f5842d1c7..a51feb6a73 100644 --- a/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs +++ b/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs @@ -5,6 +5,7 @@ using Bit.Core.NotificationCenter.Authorization; using Bit.Core.NotificationCenter.Commands; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Platform.Push; using Bit.Core.Test.NotificationCenter.AutoFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -55,5 +56,8 @@ public class CreateNotificationCommandTest Assert.Equal(notification, newNotification); Assert.Equal(DateTime.UtcNow, notification.CreationDate, TimeSpan.FromMinutes(1)); Assert.Equal(notification.CreationDate, notification.RevisionDate); + await sutProvider.GetDependency() + .Received(1) + .PushSyncNotificationAsync(newNotification); } } diff --git a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs index c26fc23460..dc391b9801 100644 --- a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs +++ b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs @@ -1,42 +1,236 @@ -using Bit.Core.NotificationHub; -using Bit.Core.Platform.Push; +#nullable enable +using System.Text.Json; +using Bit.Core.Enums; +using Bit.Core.Models; +using Bit.Core.Models.Data; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationHub; using Bit.Core.Repositories; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; namespace Bit.Core.Test.NotificationHub; +[SutProviderCustomize] public class NotificationHubPushNotificationServiceTests { - private readonly NotificationHubPushNotificationService _sut; - - private readonly IInstallationDeviceRepository _installationDeviceRepository; - private readonly INotificationHubPool _notificationHubPool; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly ILogger _logger; - - public NotificationHubPushNotificationServiceTests() + [Theory] + [BitAutoData] + [NotificationCustomize] + public async void PushSyncNotificationAsync_Global_NotSent( + SutProvider sutProvider, Notification notification) { - _installationDeviceRepository = Substitute.For(); - _httpContextAccessor = Substitute.For(); - _notificationHubPool = Substitute.For(); - _logger = Substitute.For>(); + await sutProvider.Sut.PushSyncNotificationAsync(notification); - _sut = new NotificationHubPushNotificationService( - _installationDeviceRepository, - _notificationHubPool, - _httpContextAccessor, - _logger - ); + await sutProvider.GetDependency() + .Received(0) + .AllClients + .Received(0) + .SendTemplateNotificationAsync(Arg.Any>(), Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); } - // Remove this test when we add actual tests. It only proves that - // we've properly constructed the system under test. - [Fact(Skip = "Needs additional work")] - public void ServiceExists() + [Theory] + [BitAutoData(false)] + [BitAutoData(true)] + [NotificationCustomize(false)] + public async void PushSyncNotificationAsync_UserIdProvidedClientTypeAll_SentToUser( + bool organizationIdNull, SutProvider sutProvider, + Notification notification) { - Assert.NotNull(_sut); + if (organizationIdNull) + { + notification.OrganizationId = null; + } + + notification.ClientType = ClientType.All; + var expectedSyncNotification = ToSyncNotificationPushNotification(notification); + + await sutProvider.Sut.PushSyncNotificationAsync(notification); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, expectedSyncNotification, + $"(template:payload_userId:{notification.UserId})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(false, ClientType.Browser)] + [BitAutoData(false, ClientType.Desktop)] + [BitAutoData(false, ClientType.Web)] + [BitAutoData(false, ClientType.Mobile)] + [BitAutoData(true, ClientType.Browser)] + [BitAutoData(true, ClientType.Desktop)] + [BitAutoData(true, ClientType.Web)] + [BitAutoData(true, ClientType.Mobile)] + [NotificationCustomize(false)] + public async void PushSyncNotificationAsync_UserIdProvidedClientTypeNotAll_SentToUser(bool organizationIdNull, + ClientType clientType, SutProvider sutProvider, + Notification notification) + { + if (organizationIdNull) + { + notification.OrganizationId = null; + } + + notification.ClientType = clientType; + var expectedSyncNotification = ToSyncNotificationPushNotification(notification); + + await sutProvider.Sut.PushSyncNotificationAsync(notification); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, expectedSyncNotification, + $"(template:payload_userId:{notification.UserId} && clientType:{clientType})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(false)] + public async void PushSyncNotificationAsync_UserIdNullOrganizationIdProvidedClientTypeAll_SentToOrganization( + SutProvider sutProvider, Notification notification) + { + notification.UserId = null; + notification.ClientType = ClientType.All; + var expectedSyncNotification = ToSyncNotificationPushNotification(notification); + + await sutProvider.Sut.PushSyncNotificationAsync(notification); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, expectedSyncNotification, + $"(template:payload && organizationId:{notification.OrganizationId})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(ClientType.Browser)] + [BitAutoData(ClientType.Desktop)] + [BitAutoData(ClientType.Web)] + [BitAutoData(ClientType.Mobile)] + [NotificationCustomize(false)] + public async void PushSyncNotificationAsync_UserIdNullOrganizationIdProvidedClientTypeNotAll_SentToOrganization( + ClientType clientType, SutProvider sutProvider, + Notification notification) + { + notification.UserId = null; + notification.ClientType = clientType; + + var expectedSyncNotification = ToSyncNotificationPushNotification(notification); + + await sutProvider.Sut.PushSyncNotificationAsync(notification); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, expectedSyncNotification, + $"(template:payload && organizationId:{notification.OrganizationId} && clientType:{clientType})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData([null])] + [BitAutoData(ClientType.All)] + public async void SendPayloadToUserAsync_ClientTypeNullOrAll_SentToUser(ClientType? clientType, + SutProvider sutProvider, Guid userId, PushType pushType, string payload, + string identifier) + { + await sutProvider.Sut.SendPayloadToUserAsync(userId.ToString(), pushType, payload, identifier, null, + clientType); + + await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload, + $"(template:payload_userId:{userId} && !deviceIdentifier:{identifier})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(ClientType.Browser)] + [BitAutoData(ClientType.Desktop)] + [BitAutoData(ClientType.Mobile)] + [BitAutoData(ClientType.Web)] + public async void SendPayloadToUserAsync_ClientTypeExplicit_SentToUserAndClientType(ClientType clientType, + SutProvider sutProvider, Guid userId, PushType pushType, string payload, + string identifier) + { + await sutProvider.Sut.SendPayloadToUserAsync(userId.ToString(), pushType, payload, identifier, null, + clientType); + + await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload, + $"(template:payload_userId:{userId} && !deviceIdentifier:{identifier} && clientType:{clientType})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData([null])] + [BitAutoData(ClientType.All)] + public async void SendPayloadToOrganizationAsync_ClientTypeNullOrAll_SentToOrganization(ClientType? clientType, + SutProvider sutProvider, Guid organizationId, PushType pushType, + string payload, string identifier) + { + await sutProvider.Sut.SendPayloadToOrganizationAsync(organizationId.ToString(), pushType, payload, identifier, + null, clientType); + + await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload, + $"(template:payload && organizationId:{organizationId} && !deviceIdentifier:{identifier})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(ClientType.Browser)] + [BitAutoData(ClientType.Desktop)] + [BitAutoData(ClientType.Mobile)] + [BitAutoData(ClientType.Web)] + public async void SendPayloadToOrganizationAsync_ClientTypeExplicit_SentToOrganizationAndClientType( + ClientType clientType, SutProvider sutProvider, Guid organizationId, + PushType pushType, string payload, string identifier) + { + await sutProvider.Sut.SendPayloadToOrganizationAsync(organizationId.ToString(), pushType, payload, identifier, + null, clientType); + + await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload, + $"(template:payload && organizationId:{organizationId} && !deviceIdentifier:{identifier} && clientType:{clientType})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + private static SyncNotificationPushNotification ToSyncNotificationPushNotification(Notification notification) => + new() + { + Id = notification.Id, + UserId = notification.UserId, + OrganizationId = notification.OrganizationId, + ClientType = notification.ClientType, + RevisionDate = notification.RevisionDate + }; + + private static async Task AssertSendTemplateNotificationAsync( + SutProvider sutProvider, PushType type, object payload, string tag) + { + await sutProvider.GetDependency() + .Received(1) + .AllClients + .Received(1) + .SendTemplateNotificationAsync( + Arg.Is>(dictionary => MatchingSendPayload(dictionary, type, payload)), + tag); + } + + private static bool MatchingSendPayload(IDictionary dictionary, PushType type, object payload) + { + return dictionary.ContainsKey("type") && dictionary["type"].Equals(((byte)type).ToString()) && + dictionary.ContainsKey("payload") && dictionary["payload"].Equals(JsonSerializer.Serialize(payload)); } } diff --git a/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs b/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs index c5851f2791..d51df9c882 100644 --- a/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs +++ b/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs @@ -1,44 +1,290 @@ -using Bit.Core.NotificationHub; -using Bit.Core.Repositories; -using Bit.Core.Settings; -using Microsoft.Extensions.Logging; +#nullable enable +using Bit.Core.Enums; +using Bit.Core.NotificationHub; +using Bit.Core.Utilities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Azure.NotificationHubs; using NSubstitute; using Xunit; namespace Bit.Core.Test.NotificationHub; +[SutProviderCustomize] public class NotificationHubPushRegistrationServiceTests { - private readonly NotificationHubPushRegistrationService _sut; - - private readonly IInstallationDeviceRepository _installationDeviceRepository; - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private readonly GlobalSettings _globalSettings; - private readonly INotificationHubPool _notificationHubPool; - - public NotificationHubPushRegistrationServiceTests() + [Theory] + [BitAutoData([null])] + [BitAutoData("")] + [BitAutoData(" ")] + public async Task CreateOrUpdateRegistrationAsync_PushTokenNullOrEmpty_InstallationNotCreated(string? pushToken, + SutProvider sutProvider, Guid deviceId, Guid userId, Guid identifier, + Guid organizationId) { - _installationDeviceRepository = Substitute.For(); - _serviceProvider = Substitute.For(); - _logger = Substitute.For>(); - _globalSettings = new GlobalSettings(); - _notificationHubPool = Substitute.For(); + await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), + identifier.ToString(), DeviceType.Android, [organizationId.ToString()]); - _sut = new NotificationHubPushRegistrationService( - _installationDeviceRepository, - _globalSettings, - _notificationHubPool, - _serviceProvider, - _logger - ); + sutProvider.GetDependency() + .Received(0) + .ClientFor(deviceId); } - // Remove this test when we add actual tests. It only proves that - // we've properly constructed the system under test. - [Fact(Skip = "Needs additional work")] - public void ServiceExists() + [Theory] + [BitAutoData(false, false)] + [BitAutoData(false, true)] + [BitAutoData(true, false)] + [BitAutoData(true, true)] + public async Task CreateOrUpdateRegistrationAsync_DeviceTypeAndroid_InstallationCreated(bool identifierNull, + bool partOfOrganizationId, SutProvider sutProvider, Guid deviceId, + Guid userId, Guid? identifier, Guid organizationId) { - Assert.NotNull(_sut); + var notificationHubClient = Substitute.For(); + sutProvider.GetDependency().ClientFor(Arg.Any()).Returns(notificationHubClient); + + var pushToken = "test push token"; + + await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), + identifierNull ? null : identifier.ToString(), DeviceType.Android, + partOfOrganizationId ? [organizationId.ToString()] : []); + + sutProvider.GetDependency() + .Received(1) + .ClientFor(deviceId); + await notificationHubClient + .Received(1) + .CreateOrUpdateInstallationAsync(Arg.Is(installation => + installation.InstallationId == deviceId.ToString() && + installation.PushChannel == pushToken && + installation.Platform == NotificationPlatform.FcmV1 && + installation.Tags.Contains($"userId:{userId}") && + installation.Tags.Contains("clientType:Mobile") && + (identifierNull || installation.Tags.Contains($"deviceIdentifier:{identifier}")) && + (!partOfOrganizationId || installation.Tags.Contains($"organizationId:{organizationId}")) && + installation.Templates.Count == 3)); + await notificationHubClient + .Received(1) + .CreateOrUpdateInstallationAsync(Arg.Is(installation => MatchingInstallationTemplate( + installation.Templates, "template:payload", + "{\"message\":{\"data\":{\"type\":\"$(type)\",\"payload\":\"$(payload)\"}}}", + new List + { + "template:payload", + $"template:payload_userId:{userId}", + "clientType:Mobile", + identifierNull ? null : $"template:payload_deviceIdentifier:{identifier}", + partOfOrganizationId ? $"organizationId:{organizationId}" : null, + }))); + await notificationHubClient + .Received(1) + .CreateOrUpdateInstallationAsync(Arg.Is(installation => MatchingInstallationTemplate( + installation.Templates, "template:message", + "{\"message\":{\"data\":{\"type\":\"$(type)\"},\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}", + new List + { + "template:message", + $"template:message_userId:{userId}", + "clientType:Mobile", + identifierNull ? null : $"template:message_deviceIdentifier:{identifier}", + partOfOrganizationId ? $"organizationId:{organizationId}" : null, + }))); + await notificationHubClient + .Received(1) + .CreateOrUpdateInstallationAsync(Arg.Is(installation => MatchingInstallationTemplate( + installation.Templates, "template:badgeMessage", + "{\"message\":{\"data\":{\"type\":\"$(type)\"},\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}", + new List + { + "template:badgeMessage", + $"template:badgeMessage_userId:{userId}", + "clientType:Mobile", + identifierNull ? null : $"template:badgeMessage_deviceIdentifier:{identifier}", + partOfOrganizationId ? $"organizationId:{organizationId}" : null, + }))); + } + + [Theory] + [BitAutoData(false, false)] + [BitAutoData(false, true)] + [BitAutoData(true, false)] + [BitAutoData(true, true)] + public async Task CreateOrUpdateRegistrationAsync_DeviceTypeIOS_InstallationCreated(bool identifierNull, + bool partOfOrganizationId, SutProvider sutProvider, Guid deviceId, + Guid userId, Guid identifier, Guid organizationId) + { + var notificationHubClient = Substitute.For(); + sutProvider.GetDependency().ClientFor(Arg.Any()).Returns(notificationHubClient); + + var pushToken = "test push token"; + + await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), + identifierNull ? null : identifier.ToString(), DeviceType.iOS, + partOfOrganizationId ? [organizationId.ToString()] : []); + + sutProvider.GetDependency() + .Received(1) + .ClientFor(deviceId); + await notificationHubClient + .Received(1) + .CreateOrUpdateInstallationAsync(Arg.Is(installation => + installation.InstallationId == deviceId.ToString() && + installation.PushChannel == pushToken && + installation.Platform == NotificationPlatform.Apns && + installation.Tags.Contains($"userId:{userId}") && + installation.Tags.Contains("clientType:Mobile") && + (identifierNull || installation.Tags.Contains($"deviceIdentifier:{identifier}")) && + (!partOfOrganizationId || installation.Tags.Contains($"organizationId:{organizationId}")) && + installation.Templates.Count == 3)); + await notificationHubClient + .Received(1) + .CreateOrUpdateInstallationAsync(Arg.Is(installation => MatchingInstallationTemplate( + installation.Templates, "template:payload", + "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"},\"aps\":{\"content-available\":1}}", + new List + { + "template:payload", + $"template:payload_userId:{userId}", + "clientType:Mobile", + identifierNull ? null : $"template:payload_deviceIdentifier:{identifier}", + partOfOrganizationId ? $"organizationId:{organizationId}" : null, + }))); + await notificationHubClient + .Received(1) + .CreateOrUpdateInstallationAsync(Arg.Is(installation => MatchingInstallationTemplate( + installation.Templates, "template:message", + "{\"data\":{\"type\":\"#(type)\"},\"aps\":{\"alert\":\"$(message)\",\"badge\":null,\"content-available\":1}}", + new List + { + "template:message", + $"template:message_userId:{userId}", + "clientType:Mobile", + identifierNull ? null : $"template:message_deviceIdentifier:{identifier}", + partOfOrganizationId ? $"organizationId:{organizationId}" : null, + }))); + await notificationHubClient + .Received(1) + .CreateOrUpdateInstallationAsync(Arg.Is(installation => MatchingInstallationTemplate( + installation.Templates, "template:badgeMessage", + "{\"data\":{\"type\":\"#(type)\"},\"aps\":{\"alert\":\"$(message)\",\"badge\":\"#(badge)\",\"content-available\":1}}", + new List + { + "template:badgeMessage", + $"template:badgeMessage_userId:{userId}", + "clientType:Mobile", + identifierNull ? null : $"template:badgeMessage_deviceIdentifier:{identifier}", + partOfOrganizationId ? $"organizationId:{organizationId}" : null, + }))); + } + + [Theory] + [BitAutoData(false, false)] + [BitAutoData(false, true)] + [BitAutoData(true, false)] + [BitAutoData(true, true)] + public async Task CreateOrUpdateRegistrationAsync_DeviceTypeAndroidAmazon_InstallationCreated(bool identifierNull, + bool partOfOrganizationId, SutProvider sutProvider, Guid deviceId, + Guid userId, Guid identifier, Guid organizationId) + { + var notificationHubClient = Substitute.For(); + sutProvider.GetDependency().ClientFor(Arg.Any()).Returns(notificationHubClient); + + var pushToken = "test push token"; + + await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), + identifierNull ? null : identifier.ToString(), DeviceType.AndroidAmazon, + partOfOrganizationId ? [organizationId.ToString()] : []); + + sutProvider.GetDependency() + .Received(1) + .ClientFor(deviceId); + await notificationHubClient + .Received(1) + .CreateOrUpdateInstallationAsync(Arg.Is(installation => + installation.InstallationId == deviceId.ToString() && + installation.PushChannel == pushToken && + installation.Platform == NotificationPlatform.Adm && + installation.Tags.Contains($"userId:{userId}") && + installation.Tags.Contains("clientType:Mobile") && + (identifierNull || installation.Tags.Contains($"deviceIdentifier:{identifier}")) && + (!partOfOrganizationId || installation.Tags.Contains($"organizationId:{organizationId}")) && + installation.Templates.Count == 3)); + await notificationHubClient + .Received(1) + .CreateOrUpdateInstallationAsync(Arg.Is(installation => MatchingInstallationTemplate( + installation.Templates, "template:payload", + "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}}", + new List + { + "template:payload", + $"template:payload_userId:{userId}", + "clientType:Mobile", + identifierNull ? null : $"template:payload_deviceIdentifier:{identifier}", + partOfOrganizationId ? $"organizationId:{organizationId}" : null, + }))); + await notificationHubClient + .Received(1) + .CreateOrUpdateInstallationAsync(Arg.Is(installation => MatchingInstallationTemplate( + installation.Templates, "template:message", + "{\"data\":{\"type\":\"#(type)\",\"message\":\"$(message)\"}}", + new List + { + "template:message", + $"template:message_userId:{userId}", + "clientType:Mobile", + identifierNull ? null : $"template:message_deviceIdentifier:{identifier}", + partOfOrganizationId ? $"organizationId:{organizationId}" : null, + }))); + await notificationHubClient + .Received(1) + .CreateOrUpdateInstallationAsync(Arg.Is(installation => MatchingInstallationTemplate( + installation.Templates, "template:badgeMessage", + "{\"data\":{\"type\":\"#(type)\",\"message\":\"$(message)\"}}", + new List + { + "template:badgeMessage", + $"template:badgeMessage_userId:{userId}", + "clientType:Mobile", + identifierNull ? null : $"template:badgeMessage_deviceIdentifier:{identifier}", + partOfOrganizationId ? $"organizationId:{organizationId}" : null, + }))); + } + + [Theory] + [BitAutoData(DeviceType.ChromeBrowser)] + [BitAutoData(DeviceType.ChromeExtension)] + [BitAutoData(DeviceType.MacOsDesktop)] + public async Task CreateOrUpdateRegistrationAsync_DeviceTypeNotMobile_InstallationCreated(DeviceType deviceType, + SutProvider sutProvider, Guid deviceId, Guid userId, Guid identifier, + Guid organizationId) + { + var notificationHubClient = Substitute.For(); + sutProvider.GetDependency().ClientFor(Arg.Any()).Returns(notificationHubClient); + + var pushToken = "test push token"; + + await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), + identifier.ToString(), deviceType, [organizationId.ToString()]); + + sutProvider.GetDependency() + .Received(1) + .ClientFor(deviceId); + await notificationHubClient + .Received(1) + .CreateOrUpdateInstallationAsync(Arg.Is(installation => + installation.InstallationId == deviceId.ToString() && + installation.PushChannel == pushToken && + installation.Tags.Contains($"userId:{userId}") && + installation.Tags.Contains($"clientType:{DeviceTypes.ToClientType(deviceType)}") && + installation.Tags.Contains($"deviceIdentifier:{identifier}") && + installation.Tags.Contains($"organizationId:{organizationId}") && + installation.Templates.Count == 0)); + } + + private static bool MatchingInstallationTemplate(IDictionary templates, string key, + string body, List tags) + { + var tagsNoNulls = tags.FindAll(tag => tag != null); + return templates.ContainsKey(key) && templates[key].Body == body && + templates[key].Tags.Count == tagsNoNulls.Count && + templates[key].Tags.All(tagsNoNulls.Contains); } } diff --git a/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs index 85ce5a79ac..7aa053ec6d 100644 --- a/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs @@ -1,33 +1,66 @@ -using Bit.Core.Settings; +#nullable enable +using System.Text.Json; +using Azure.Storage.Queues; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Models; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.Test.AutoFixture; +using Bit.Core.Test.AutoFixture.CurrentContextFixtures; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Http; using NSubstitute; using Xunit; namespace Bit.Core.Platform.Push.Internal.Test; +[QueueClientCustomize] +[SutProviderCustomize] public class AzureQueuePushNotificationServiceTests { - private readonly AzureQueuePushNotificationService _sut; - - private readonly GlobalSettings _globalSettings; - private readonly IHttpContextAccessor _httpContextAccessor; - - public AzureQueuePushNotificationServiceTests() + [Theory] + [BitAutoData] + [NotificationCustomize] + [CurrentContextCustomize] + public async void PushSyncNotificationAsync_Notification_Sent( + SutProvider sutProvider, Notification notification, Guid deviceIdentifier, + ICurrentContext currentContext) { - _globalSettings = new GlobalSettings(); - _httpContextAccessor = Substitute.For(); + currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); + sutProvider.GetDependency().HttpContext!.RequestServices + .GetService(Arg.Any()).Returns(currentContext); - _sut = new AzureQueuePushNotificationService( - _globalSettings, - _httpContextAccessor - ); + await sutProvider.Sut.PushSyncNotificationAsync(notification); + + await sutProvider.GetDependency().Received(1) + .SendMessageAsync(Arg.Is(message => + MatchMessage(PushType.SyncNotification, message, new SyncNotificationEquals(notification), + deviceIdentifier.ToString()))); } - // Remove this test when we add actual tests. It only proves that - // we've properly constructed the system under test. - [Fact(Skip = "Needs additional work")] - public void ServiceExists() + private static bool MatchMessage(PushType pushType, string message, IEquatable expectedPayloadEquatable, + string contextId) { - Assert.NotNull(_sut); + var pushNotificationData = + JsonSerializer.Deserialize>(message); + return pushNotificationData != null && + pushNotificationData.Type == pushType && + expectedPayloadEquatable.Equals(pushNotificationData.Payload) && + pushNotificationData.ContextId == contextId; + } + + private class SyncNotificationEquals(Notification notification) : IEquatable + { + public bool Equals(SyncNotificationPushNotification? other) + { + return other != null && + other.Id == notification.Id && + other.UserId == notification.UserId && + other.OrganizationId == notification.OrganizationId && + other.ClientType == notification.ClientType && + other.RevisionDate == notification.RevisionDate; + } } } diff --git a/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs index 021aa7f2cc..35997f80e9 100644 --- a/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs @@ -1,44 +1,62 @@ -using AutoFixture; +#nullable enable +using Bit.Core.Enums; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.Test.NotificationCenter.AutoFixture; using Bit.Test.Common.AutoFixture; -using Microsoft.Extensions.Logging; +using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; -using GlobalSettingsCustomization = Bit.Test.Common.AutoFixture.GlobalSettings; namespace Bit.Core.Platform.Push.Internal.Test; +[SutProviderCustomize] public class MultiServicePushNotificationServiceTests { - private readonly MultiServicePushNotificationService _sut; - - private readonly ILogger _logger; - private readonly ILogger _relayLogger; - private readonly ILogger _hubLogger; - private readonly IEnumerable _services; - private readonly Settings.GlobalSettings _globalSettings; - - public MultiServicePushNotificationServiceTests() + [Theory] + [BitAutoData] + [NotificationCustomize] + public async Task PushSyncNotificationAsync_Notification_Sent( + SutProvider sutProvider, Notification notification) { - _logger = Substitute.For>(); - _relayLogger = Substitute.For>(); - _hubLogger = Substitute.For>(); + await sutProvider.Sut.PushSyncNotificationAsync(notification); - var fixture = new Fixture().WithAutoNSubstitutions().Customize(new GlobalSettingsCustomization()); - _services = fixture.CreateMany(); - _globalSettings = fixture.Create(); - - _sut = new MultiServicePushNotificationService( - _services, - _logger, - _globalSettings - ); + await sutProvider.GetDependency>() + .First() + .Received(1) + .PushSyncNotificationAsync(notification); } - // Remove this test when we add actual tests. It only proves that - // we've properly constructed the system under test. - [Fact] - public void ServiceExists() + [Theory] + [BitAutoData([null, null])] + [BitAutoData(ClientType.All, null)] + [BitAutoData([null, "test device id"])] + [BitAutoData(ClientType.All, "test device id")] + public async Task SendPayloadToUserAsync_Message_Sent(ClientType? clientType, string? deviceId, string userId, + PushType type, object payload, string identifier, SutProvider sutProvider) { - Assert.NotNull(_sut); + await sutProvider.Sut.SendPayloadToUserAsync(userId, type, payload, identifier, deviceId, clientType); + + await sutProvider.GetDependency>() + .First() + .Received(1) + .SendPayloadToUserAsync(userId, type, payload, identifier, deviceId, clientType); + } + + [Theory] + [BitAutoData([null, null])] + [BitAutoData(ClientType.All, null)] + [BitAutoData([null, "test device id"])] + [BitAutoData(ClientType.All, "test device id")] + public async Task SendPayloadToOrganizationAsync_Message_Sent(ClientType? clientType, string? deviceId, + string organizationId, PushType type, object payload, string identifier, + SutProvider sutProvider) + { + await sutProvider.Sut.SendPayloadToOrganizationAsync(organizationId, type, payload, identifier, deviceId, + clientType); + + await sutProvider.GetDependency>() + .First() + .Received(1) + .SendPayloadToOrganizationAsync(organizationId, type, payload, identifier, deviceId, clientType); } } diff --git a/test/Core.Test/Services/DeviceServiceTests.cs b/test/Core.Test/Services/DeviceServiceTests.cs index 41ef0b4d74..98b04eb7d3 100644 --- a/test/Core.Test/Services/DeviceServiceTests.cs +++ b/test/Core.Test/Services/DeviceServiceTests.cs @@ -3,6 +3,7 @@ using Bit.Core.Auth.Models.Api.Request; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; @@ -16,15 +17,23 @@ namespace Bit.Core.Test.Services; [SutProviderCustomize] public class DeviceServiceTests { - [Fact] - public async Task DeviceSaveShouldUpdateRevisionDateAndPushRegistration() + [Theory] + [BitAutoData] + public async Task SaveAsync_IdProvided_UpdatedRevisionDateAndPushRegistration(Guid id, Guid userId, + Guid organizationId1, Guid organizationId2, + OrganizationUserOrganizationDetails organizationUserOrganizationDetails1, + OrganizationUserOrganizationDetails organizationUserOrganizationDetails2) { + organizationUserOrganizationDetails1.OrganizationId = organizationId1; + organizationUserOrganizationDetails2.OrganizationId = organizationId2; + var deviceRepo = Substitute.For(); var pushRepo = Substitute.For(); - var deviceService = new DeviceService(deviceRepo, pushRepo); + var organizationUserRepository = Substitute.For(); + organizationUserRepository.GetManyDetailsByUserAsync(Arg.Any(), Arg.Any()) + .Returns([organizationUserOrganizationDetails1, organizationUserOrganizationDetails2]); + var deviceService = new DeviceService(deviceRepo, pushRepo, organizationUserRepository); - var id = Guid.NewGuid(); - var userId = Guid.NewGuid(); var device = new Device { Id = id, @@ -37,8 +46,53 @@ public class DeviceServiceTests await deviceService.SaveAsync(device); Assert.True(device.RevisionDate - DateTime.UtcNow < TimeSpan.FromSeconds(1)); - await pushRepo.Received().CreateOrUpdateRegistrationAsync("testtoken", id.ToString(), - userId.ToString(), "testid", DeviceType.Android); + await pushRepo.Received(1).CreateOrUpdateRegistrationAsync("testtoken", id.ToString(), + userId.ToString(), "testid", DeviceType.Android, + Arg.Do>(organizationIds => + { + var organizationIdsList = organizationIds.ToList(); + Assert.Equal(2, organizationIdsList.Count); + Assert.Contains(organizationId1.ToString(), organizationIdsList); + Assert.Contains(organizationId2.ToString(), organizationIdsList); + })); + } + + [Theory] + [BitAutoData] + public async Task SaveAsync_IdNotProvided_CreatedAndPushRegistration(Guid userId, Guid organizationId1, + Guid organizationId2, + OrganizationUserOrganizationDetails organizationUserOrganizationDetails1, + OrganizationUserOrganizationDetails organizationUserOrganizationDetails2) + { + organizationUserOrganizationDetails1.OrganizationId = organizationId1; + organizationUserOrganizationDetails2.OrganizationId = organizationId2; + + var deviceRepo = Substitute.For(); + var pushRepo = Substitute.For(); + var organizationUserRepository = Substitute.For(); + organizationUserRepository.GetManyDetailsByUserAsync(Arg.Any(), Arg.Any()) + .Returns([organizationUserOrganizationDetails1, organizationUserOrganizationDetails2]); + var deviceService = new DeviceService(deviceRepo, pushRepo, organizationUserRepository); + + var device = new Device + { + Name = "test device", + Type = DeviceType.Android, + UserId = userId, + PushToken = "testtoken", + Identifier = "testid" + }; + await deviceService.SaveAsync(device); + + await pushRepo.Received(1).CreateOrUpdateRegistrationAsync("testtoken", + Arg.Do(id => Guid.TryParse(id, out var _)), userId.ToString(), "testid", DeviceType.Android, + Arg.Do>(organizationIds => + { + var organizationIdsList = organizationIds.ToList(); + Assert.Equal(2, organizationIdsList.Count); + Assert.Contains(organizationId1.ToString(), organizationIdsList); + Assert.Contains(organizationId2.ToString(), organizationIdsList); + })); } /// @@ -62,12 +116,7 @@ public class DeviceServiceTests sutProvider.GetDependency() .GetManyByUserIdAsync(currentUserId) - .Returns(new List - { - deviceOne, - deviceTwo, - deviceThree, - }); + .Returns(new List { deviceOne, deviceTwo, deviceThree, }); var currentDeviceModel = new DeviceKeysUpdateRequestModel { @@ -85,7 +134,8 @@ public class DeviceServiceTests }, }; - await sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel, alteredDeviceModels); + await sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel, + alteredDeviceModels); // Updating trust, "current" or "other" only needs to change the EncryptedPublicKey & EncryptedUserKey await sutProvider.GetDependency() @@ -149,11 +199,7 @@ public class DeviceServiceTests sutProvider.GetDependency() .GetManyByUserIdAsync(currentUserId) - .Returns(new List - { - deviceOne, - deviceTwo, - }); + .Returns(new List { deviceOne, deviceTwo, }); var currentDeviceModel = new DeviceKeysUpdateRequestModel { @@ -171,7 +217,8 @@ public class DeviceServiceTests }, }; - await sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel, alteredDeviceModels); + await sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel, + alteredDeviceModels); // Check that UpsertAsync was called for the trusted device await sutProvider.GetDependency() @@ -203,11 +250,7 @@ public class DeviceServiceTests sutProvider.GetDependency() .GetManyByUserIdAsync(currentUserId) - .Returns(new List - { - deviceOne, - deviceTwo, - }); + .Returns(new List { deviceOne, deviceTwo, }); var currentDeviceModel = new DeviceKeysUpdateRequestModel { @@ -237,11 +280,7 @@ public class DeviceServiceTests sutProvider.GetDependency() .GetManyByUserIdAsync(currentUserId) - .Returns(new List - { - deviceOne, - deviceTwo, - }); + .Returns(new List { deviceOne, deviceTwo, }); var currentDeviceModel = new DeviceKeysUpdateRequestModel { @@ -260,6 +299,7 @@ public class DeviceServiceTests }; await Assert.ThrowsAsync(() => - sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel, alteredDeviceModels)); + sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel, + alteredDeviceModels)); } } diff --git a/test/Identity.IntegrationTest/openid-configuration.json b/test/Identity.IntegrationTest/openid-configuration.json index 23e5a67c06..4d74f66009 100644 --- a/test/Identity.IntegrationTest/openid-configuration.json +++ b/test/Identity.IntegrationTest/openid-configuration.json @@ -24,6 +24,7 @@ "sstamp", "premium", "device", + "devicetype", "orgowner", "orgadmin", "orguser", From b98b74cef6135af29a3152def0960baa948745be Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:31:03 +0100 Subject: [PATCH 126/148] [PM-10600] Push notification with full notification center content (#5086) * PM-10600: Notification push notification * PM-10600: Sending to specific client types for relay push notifications * PM-10600: Sending to specific client types for other clients * PM-10600: Send push notification on notification creation * PM-10600: Explicit group names * PM-10600: Id typos * PM-10600: Revert global push notifications * PM-10600: Added DeviceType claim * PM-10600: Sent to organization typo * PM-10600: UT coverage * PM-10600: Small refactor, UTs coverage * PM-10600: UTs coverage * PM-10600: Startup fix * PM-10600: Test fix * PM-10600: Required attribute, organization group for push notification fix * PM-10600: UT coverage * PM-10600: Fix Mobile devices not registering to organization push notifications We only register devices for organization push notifications when the organization is being created. This does not work, since we have a use case (Notification Center) of delivering notifications to all users of organization. This fixes it, by adding the organization id tag when device registers for push notifications. * PM-10600: Unit Test coverage for NotificationHubPushRegistrationService Fixed IFeatureService substitute mocking for Android tests. Added user part of organization test with organizationId tags expectation. * PM-10600: Unit Tests fix to NotificationHubPushRegistrationService after merge conflict * PM-10600: Organization push notifications not sending to mobile device from self-hosted. Self-hosted instance uses relay to register the mobile device against Bitwarden Cloud Api. Only the self-hosted server knows client's organization membership, which means it needs to pass in the organization id's information to the relay. Similarly, for Bitwarden Cloud, the organizaton id will come directly from the server. * PM-10600: Fix self-hosted organization notification not being received by mobile device. When mobile device registers on self-hosted through the relay, every single id, like user id, device id and now organization id needs to be prefixed with the installation id. This have been missing in the PushController that handles this for organization id. * 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. * PM-10600: Merge conflicts fix * merge conflict fix * PM-10600: Push notification with full notification center content. Notification Center push notification now includes all the fields. --- src/Core/Models/PushNotification.cs | 12 ++++++-- .../Commands/CreateNotificationCommand.cs | 2 +- .../Entities/Notification.cs | 5 ++-- .../NotificationHubPushNotificationService.cs | 11 +++++-- .../AzureQueuePushNotificationService.cs | 11 +++++-- .../Push/Services/IPushNotificationService.cs | 2 +- .../MultiServicePushNotificationService.cs | 4 +-- .../Services/NoopPushNotificationService.cs | 2 +- ...NotificationsApiPushNotificationService.cs | 11 +++++-- .../Services/RelayPushNotificationService.cs | 11 +++++-- src/Notifications/HubHelpers.cs | 2 +- .../Commands/CreateNotificationCommandTest.cs | 2 +- ...ficationHubPushNotificationServiceTests.cs | 29 +++++++++++-------- .../AzureQueuePushNotificationServiceTests.cs | 22 +++++++++----- ...ultiServicePushNotificationServiceTests.cs | 6 ++-- 15 files changed, 85 insertions(+), 47 deletions(-) diff --git a/src/Core/Models/PushNotification.cs b/src/Core/Models/PushNotification.cs index fd27ced6c5..a6e8852e95 100644 --- a/src/Core/Models/PushNotification.cs +++ b/src/Core/Models/PushNotification.cs @@ -1,4 +1,5 @@ using Bit.Core.Enums; +using Bit.Core.NotificationCenter.Enums; namespace Bit.Core.Models; @@ -45,14 +46,21 @@ public class SyncSendPushNotification public DateTime RevisionDate { get; set; } } -public class SyncNotificationPushNotification +#nullable enable +public class NotificationPushNotification { public Guid Id { get; set; } + public Priority Priority { get; set; } + public bool Global { get; set; } + public ClientType ClientType { get; set; } public Guid? UserId { get; set; } public Guid? OrganizationId { get; set; } - public ClientType ClientType { get; set; } + public string? Title { get; set; } + public string? Body { get; set; } + public DateTime CreationDate { get; set; } public DateTime RevisionDate { get; set; } } +#nullable disable public class AuthRequestPushNotification { diff --git a/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs b/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs index f378a3688a..3fddafcdc7 100644 --- a/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs +++ b/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs @@ -37,7 +37,7 @@ public class CreateNotificationCommand : ICreateNotificationCommand var newNotification = await _notificationRepository.CreateAsync(notification); - await _pushNotificationService.PushSyncNotificationAsync(newNotification); + await _pushNotificationService.PushNotificationAsync(newNotification); return newNotification; } diff --git a/src/Core/NotificationCenter/Entities/Notification.cs b/src/Core/NotificationCenter/Entities/Notification.cs index aba456efbe..ad43299f55 100644 --- a/src/Core/NotificationCenter/Entities/Notification.cs +++ b/src/Core/NotificationCenter/Entities/Notification.cs @@ -15,9 +15,8 @@ public class Notification : ITableObject public ClientType ClientType { get; set; } public Guid? UserId { get; set; } public Guid? OrganizationId { get; set; } - [MaxLength(256)] - public string? Title { get; set; } - public string? Body { get; set; } + [MaxLength(256)] public string? Title { get; set; } + [MaxLength(3000)] public string? Body { get; set; } public DateTime CreationDate { get; set; } public DateTime RevisionDate { get; set; } public Guid? TaskId { get; set; } diff --git a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs index ed44e69218..d1c7749d9f 100644 --- a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs @@ -181,14 +181,19 @@ public class NotificationHubPushNotificationService : IPushNotificationService await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse); } - public async Task PushSyncNotificationAsync(Notification notification) + public async Task PushNotificationAsync(Notification notification) { - var message = new SyncNotificationPushNotification + var message = new NotificationPushNotification { Id = notification.Id, + Priority = notification.Priority, + Global = notification.Global, + ClientType = notification.ClientType, UserId = notification.UserId, OrganizationId = notification.OrganizationId, - ClientType = notification.ClientType, + Title = notification.Title, + Body = notification.Body, + CreationDate = notification.CreationDate, RevisionDate = notification.RevisionDate }; diff --git a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs index d3509c5437..a25a017192 100644 --- a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs +++ b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs @@ -165,14 +165,19 @@ public class AzureQueuePushNotificationService : IPushNotificationService await PushSendAsync(send, PushType.SyncSendDelete); } - public async Task PushSyncNotificationAsync(Notification notification) + public async Task PushNotificationAsync(Notification notification) { - var message = new SyncNotificationPushNotification + var message = new NotificationPushNotification { Id = notification.Id, + Priority = notification.Priority, + Global = notification.Global, + ClientType = notification.ClientType, UserId = notification.UserId, OrganizationId = notification.OrganizationId, - ClientType = notification.ClientType, + Title = notification.Title, + Body = notification.Body, + CreationDate = notification.CreationDate, RevisionDate = notification.RevisionDate }; diff --git a/src/Core/Platform/Push/Services/IPushNotificationService.cs b/src/Core/Platform/Push/Services/IPushNotificationService.cs index 5e1ab7067e..7f2b6c90fe 100644 --- a/src/Core/Platform/Push/Services/IPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/IPushNotificationService.cs @@ -24,7 +24,7 @@ public interface IPushNotificationService Task PushSyncSendCreateAsync(Send send); Task PushSyncSendUpdateAsync(Send send); Task PushSyncSendDeleteAsync(Send send); - Task PushSyncNotificationAsync(Notification notification); + Task PushNotificationAsync(Notification notification); Task PushAuthRequestAsync(AuthRequest authRequest); Task PushAuthRequestResponseAsync(AuthRequest authRequest); Task PushSyncOrganizationStatusAsync(Organization organization); diff --git a/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs b/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs index 4ad81e223b..db3107b6c3 100644 --- a/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs +++ b/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs @@ -144,9 +144,9 @@ public class MultiServicePushNotificationService : IPushNotificationService return Task.CompletedTask; } - public Task PushSyncNotificationAsync(Notification notification) + public Task PushNotificationAsync(Notification notification) { - PushToServices((s) => s.PushSyncNotificationAsync(notification)); + PushToServices((s) => s.PushNotificationAsync(notification)); return Task.CompletedTask; } diff --git a/src/Core/Platform/Push/Services/NoopPushNotificationService.cs b/src/Core/Platform/Push/Services/NoopPushNotificationService.cs index 463a2fde88..fb4121179f 100644 --- a/src/Core/Platform/Push/Services/NoopPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/NoopPushNotificationService.cs @@ -113,5 +113,5 @@ public class NoopPushNotificationService : IPushNotificationService return Task.FromResult(0); } - public Task PushSyncNotificationAsync(Notification notification) => Task.CompletedTask; + public Task PushNotificationAsync(Notification notification) => Task.CompletedTask; } diff --git a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs index 5c6b46f63e..fb3814fd64 100644 --- a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs @@ -184,14 +184,19 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService await PushSendAsync(send, PushType.SyncSendDelete); } - public async Task PushSyncNotificationAsync(Notification notification) + public async Task PushNotificationAsync(Notification notification) { - var message = new SyncNotificationPushNotification + var message = new NotificationPushNotification { Id = notification.Id, + Priority = notification.Priority, + Global = notification.Global, + ClientType = notification.ClientType, UserId = notification.UserId, OrganizationId = notification.OrganizationId, - ClientType = notification.ClientType, + Title = notification.Title, + Body = notification.Body, + CreationDate = notification.CreationDate, RevisionDate = notification.RevisionDate }; diff --git a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs index f51ab004a6..4d99f04768 100644 --- a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs @@ -191,14 +191,19 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti await SendPayloadToUserAsync(authRequest.UserId, type, message, true); } - public async Task PushSyncNotificationAsync(Notification notification) + public async Task PushNotificationAsync(Notification notification) { - var message = new SyncNotificationPushNotification + var message = new NotificationPushNotification { Id = notification.Id, + Priority = notification.Priority, + Global = notification.Global, + ClientType = notification.ClientType, UserId = notification.UserId, OrganizationId = notification.OrganizationId, - ClientType = notification.ClientType, + Title = notification.Title, + Body = notification.Body, + CreationDate = notification.CreationDate, RevisionDate = notification.RevisionDate }; diff --git a/src/Notifications/HubHelpers.cs b/src/Notifications/HubHelpers.cs index 6d17ca9955..9b0164fdc5 100644 --- a/src/Notifications/HubHelpers.cs +++ b/src/Notifications/HubHelpers.cs @@ -105,7 +105,7 @@ public static class HubHelpers break; case PushType.SyncNotification: var syncNotification = - JsonSerializer.Deserialize>( + JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); if (syncNotification.Payload.UserId.HasValue) { diff --git a/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs index a51feb6a73..41efce82ab 100644 --- a/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs +++ b/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs @@ -58,6 +58,6 @@ public class CreateNotificationCommandTest Assert.Equal(notification.CreationDate, notification.RevisionDate); await sutProvider.GetDependency() .Received(1) - .PushSyncNotificationAsync(newNotification); + .PushNotificationAsync(newNotification); } } diff --git a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs index dc391b9801..f1cfdc9f85 100644 --- a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs +++ b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs @@ -20,10 +20,10 @@ public class NotificationHubPushNotificationServiceTests [Theory] [BitAutoData] [NotificationCustomize] - public async void PushSyncNotificationAsync_Global_NotSent( + public async void PushNotificationAsync_Global_NotSent( SutProvider sutProvider, Notification notification) { - await sutProvider.Sut.PushSyncNotificationAsync(notification); + await sutProvider.Sut.PushNotificationAsync(notification); await sutProvider.GetDependency() .Received(0) @@ -39,7 +39,7 @@ public class NotificationHubPushNotificationServiceTests [BitAutoData(false)] [BitAutoData(true)] [NotificationCustomize(false)] - public async void PushSyncNotificationAsync_UserIdProvidedClientTypeAll_SentToUser( + public async void PushNotificationAsync_UserIdProvidedClientTypeAll_SentToUser( bool organizationIdNull, SutProvider sutProvider, Notification notification) { @@ -51,7 +51,7 @@ public class NotificationHubPushNotificationServiceTests notification.ClientType = ClientType.All; var expectedSyncNotification = ToSyncNotificationPushNotification(notification); - await sutProvider.Sut.PushSyncNotificationAsync(notification); + await sutProvider.Sut.PushNotificationAsync(notification); await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, expectedSyncNotification, $"(template:payload_userId:{notification.UserId})"); @@ -70,7 +70,7 @@ public class NotificationHubPushNotificationServiceTests [BitAutoData(true, ClientType.Web)] [BitAutoData(true, ClientType.Mobile)] [NotificationCustomize(false)] - public async void PushSyncNotificationAsync_UserIdProvidedClientTypeNotAll_SentToUser(bool organizationIdNull, + public async void PushNotificationAsync_UserIdProvidedClientTypeNotAll_SentToUser(bool organizationIdNull, ClientType clientType, SutProvider sutProvider, Notification notification) { @@ -82,7 +82,7 @@ public class NotificationHubPushNotificationServiceTests notification.ClientType = clientType; var expectedSyncNotification = ToSyncNotificationPushNotification(notification); - await sutProvider.Sut.PushSyncNotificationAsync(notification); + await sutProvider.Sut.PushNotificationAsync(notification); await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, expectedSyncNotification, $"(template:payload_userId:{notification.UserId} && clientType:{clientType})"); @@ -94,14 +94,14 @@ public class NotificationHubPushNotificationServiceTests [Theory] [BitAutoData] [NotificationCustomize(false)] - public async void PushSyncNotificationAsync_UserIdNullOrganizationIdProvidedClientTypeAll_SentToOrganization( + public async void PushNotificationAsync_UserIdNullOrganizationIdProvidedClientTypeAll_SentToOrganization( SutProvider sutProvider, Notification notification) { notification.UserId = null; notification.ClientType = ClientType.All; var expectedSyncNotification = ToSyncNotificationPushNotification(notification); - await sutProvider.Sut.PushSyncNotificationAsync(notification); + await sutProvider.Sut.PushNotificationAsync(notification); await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, expectedSyncNotification, $"(template:payload && organizationId:{notification.OrganizationId})"); @@ -116,7 +116,7 @@ public class NotificationHubPushNotificationServiceTests [BitAutoData(ClientType.Web)] [BitAutoData(ClientType.Mobile)] [NotificationCustomize(false)] - public async void PushSyncNotificationAsync_UserIdNullOrganizationIdProvidedClientTypeNotAll_SentToOrganization( + public async void PushNotificationAsync_UserIdNullOrganizationIdProvidedClientTypeNotAll_SentToOrganization( ClientType clientType, SutProvider sutProvider, Notification notification) { @@ -125,7 +125,7 @@ public class NotificationHubPushNotificationServiceTests var expectedSyncNotification = ToSyncNotificationPushNotification(notification); - await sutProvider.Sut.PushSyncNotificationAsync(notification); + await sutProvider.Sut.PushNotificationAsync(notification); await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, expectedSyncNotification, $"(template:payload && organizationId:{notification.OrganizationId} && clientType:{clientType})"); @@ -206,13 +206,18 @@ public class NotificationHubPushNotificationServiceTests .UpsertAsync(Arg.Any()); } - private static SyncNotificationPushNotification ToSyncNotificationPushNotification(Notification notification) => + private static NotificationPushNotification ToSyncNotificationPushNotification(Notification notification) => new() { Id = notification.Id, + Priority = notification.Priority, + Global = notification.Global, + ClientType = notification.ClientType, UserId = notification.UserId, OrganizationId = notification.OrganizationId, - ClientType = notification.ClientType, + Title = notification.Title, + Body = notification.Body, + CreationDate = notification.CreationDate, RevisionDate = notification.RevisionDate }; diff --git a/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs index 7aa053ec6d..a84a76152a 100644 --- a/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs @@ -24,7 +24,7 @@ public class AzureQueuePushNotificationServiceTests [BitAutoData] [NotificationCustomize] [CurrentContextCustomize] - public async void PushSyncNotificationAsync_Notification_Sent( + public async void PushNotificationAsync_Notification_Sent( SutProvider sutProvider, Notification notification, Guid deviceIdentifier, ICurrentContext currentContext) { @@ -32,7 +32,7 @@ public class AzureQueuePushNotificationServiceTests sutProvider.GetDependency().HttpContext!.RequestServices .GetService(Arg.Any()).Returns(currentContext); - await sutProvider.Sut.PushSyncNotificationAsync(notification); + await sutProvider.Sut.PushNotificationAsync(notification); await sutProvider.GetDependency().Received(1) .SendMessageAsync(Arg.Is(message => @@ -43,23 +43,29 @@ public class AzureQueuePushNotificationServiceTests private static bool MatchMessage(PushType pushType, string message, IEquatable expectedPayloadEquatable, string contextId) { - var pushNotificationData = - JsonSerializer.Deserialize>(message); + var pushNotificationData = JsonSerializer.Deserialize>(message); return pushNotificationData != null && pushNotificationData.Type == pushType && expectedPayloadEquatable.Equals(pushNotificationData.Payload) && pushNotificationData.ContextId == contextId; } - private class SyncNotificationEquals(Notification notification) : IEquatable + private class SyncNotificationEquals(Notification notification) : IEquatable { - public bool Equals(SyncNotificationPushNotification? other) + public bool Equals(NotificationPushNotification? other) { return other != null && other.Id == notification.Id && - other.UserId == notification.UserId && - other.OrganizationId == notification.OrganizationId && + other.Priority == notification.Priority && + other.Global == notification.Global && other.ClientType == notification.ClientType && + other.UserId.HasValue == notification.UserId.HasValue && + other.UserId == notification.UserId && + other.OrganizationId.HasValue == notification.OrganizationId.HasValue && + other.OrganizationId == notification.OrganizationId && + other.Title == notification.Title && + other.Body == notification.Body && + other.CreationDate == notification.CreationDate && other.RevisionDate == notification.RevisionDate; } } diff --git a/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs index 35997f80e9..edbd297708 100644 --- a/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs @@ -15,15 +15,15 @@ public class MultiServicePushNotificationServiceTests [Theory] [BitAutoData] [NotificationCustomize] - public async Task PushSyncNotificationAsync_Notification_Sent( + public async Task PushNotificationAsync_Notification_Sent( SutProvider sutProvider, Notification notification) { - await sutProvider.Sut.PushSyncNotificationAsync(notification); + await sutProvider.Sut.PushNotificationAsync(notification); await sutProvider.GetDependency>() .First() .Received(1) - .PushSyncNotificationAsync(notification); + .PushNotificationAsync(notification); } [Theory] From 71f293138edefb06cd74d627e5dcfa9f6f23702f Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Wed, 12 Feb 2025 11:39:17 -0500 Subject: [PATCH 127/148] Remove extra BWA sync flags (#5396) --- src/Core/Constants.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index a025bbb142..6b3d0485b0 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -159,15 +159,11 @@ public static class FeatureFlagKeys public const string InlineMenuTotp = "inline-menu-totp"; public const string SelfHostLicenseRefactor = "pm-11516-self-host-license-refactor"; 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 const string AppReviewPrompt = "app-review-prompt"; public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs"; public const string Argon2Default = "argon2-default"; public const string UsePricingService = "use-pricing-service"; public const string RecordInstallationLastActivityDate = "installation-last-activity-date"; - public const string EnablePasswordManagerSyncAndroid = "enable-password-manager-sync-android"; - public const string EnablePasswordManagerSynciOS = "enable-password-manager-sync-ios"; public const string AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner"; public const string SingleTapPasskeyCreation = "single-tap-passkey-creation"; public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication"; From 5d3294c3768aea9312b97691e6216464f496795a Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 12 Feb 2025 13:42:24 -0500 Subject: [PATCH 128/148] Fix issue with credit card payment (#5399) --- .../PremiumUserBillingService.cs | 62 +++++++++++-------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index 6f571950f5..6b9f32e8f9 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -92,32 +92,7 @@ public class PremiumUserBillingService( * If the customer was previously set up with credit, which does not require a billing location, * we need to update the customer on the fly before we start the subscription. */ - if (customerSetup is - { - TokenizedPaymentSource.Type: PaymentMethodType.Credit, - TaxInformation: { Country: not null and not "", PostalCode: not null and not "" } - }) - { - var options = new CustomerUpdateOptions - { - 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, - }, - Expand = ["tax"], - Tax = new CustomerTaxOptions - { - ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately - } - }; - - customer = await stripeAdapter.CustomerUpdateAsync(customer.Id, options); - } + customer = await ReconcileBillingLocationAsync(customer, customerSetup.TaxInformation); var subscription = await CreateSubscriptionAsync(user.Id, customer, storage); @@ -167,6 +142,11 @@ public class PremiumUserBillingService( User user, CustomerSetup customerSetup) { + /* + * Creating a Customer via the adding of a payment method or the purchasing of a subscription requires + * an actual payment source. The only time this is not the case is when the Customer is created when the + * User purchases credit. + */ if (customerSetup.TokenizedPaymentSource is not { Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal, @@ -367,4 +347,34 @@ public class PremiumUserBillingService( return subscription; } + + private async Task ReconcileBillingLocationAsync( + Customer customer, + TaxInformation taxInformation) + { + if (customer is { Address: { Country: not null and not "", PostalCode: not null and not "" } }) + { + return customer; + } + + var options = new CustomerUpdateOptions + { + Address = new AddressOptions + { + Line1 = taxInformation.Line1, + Line2 = taxInformation.Line2, + City = taxInformation.City, + PostalCode = taxInformation.PostalCode, + State = taxInformation.State, + Country = taxInformation.Country, + }, + Expand = ["tax"], + Tax = new CustomerTaxOptions + { + ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately + } + }; + + return await stripeAdapter.CustomerUpdateAsync(customer.Id, options); + } } From 459c91a5a95b882c9eb6dd39979c05cc39035304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:30:50 +0000 Subject: [PATCH 129/148] [PM-13748] Remove SCIM provider type checks from group endpoints (#5231) * [PM-13748] Remove SCIM provider type checks from group endpoints * Remove Okta provider type config from group command tests --------- Co-authored-by: bnagawiecki <107435978+bnagawiecki@users.noreply.github.com> --- .../src/Scim/Groups/PostGroupCommand.cs | 12 ------------ bitwarden_license/src/Scim/Groups/PutGroupCommand.cs | 11 ----------- .../test/Scim.Test/Groups/PostGroupCommandTests.cs | 6 ------ .../test/Scim.Test/Groups/PutGroupCommandTests.cs | 6 ------ 4 files changed, 35 deletions(-) diff --git a/bitwarden_license/src/Scim/Groups/PostGroupCommand.cs b/bitwarden_license/src/Scim/Groups/PostGroupCommand.cs index 9cc8c8d13c..5a7a03f1f4 100644 --- a/bitwarden_license/src/Scim/Groups/PostGroupCommand.cs +++ b/bitwarden_license/src/Scim/Groups/PostGroupCommand.cs @@ -1,11 +1,8 @@ using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Repositories; -using Bit.Scim.Context; using Bit.Scim.Groups.Interfaces; using Bit.Scim.Models; @@ -14,17 +11,13 @@ namespace Bit.Scim.Groups; public class PostGroupCommand : IPostGroupCommand { private readonly IGroupRepository _groupRepository; - private readonly IScimContext _scimContext; private readonly ICreateGroupCommand _createGroupCommand; public PostGroupCommand( IGroupRepository groupRepository, - IOrganizationRepository organizationRepository, - IScimContext scimContext, ICreateGroupCommand createGroupCommand) { _groupRepository = groupRepository; - _scimContext = scimContext; _createGroupCommand = createGroupCommand; } @@ -50,11 +43,6 @@ public class PostGroupCommand : IPostGroupCommand private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model) { - if (_scimContext.RequestScimProvider != ScimProviderType.Okta) - { - return; - } - if (model.Members == null) { return; diff --git a/bitwarden_license/src/Scim/Groups/PutGroupCommand.cs b/bitwarden_license/src/Scim/Groups/PutGroupCommand.cs index 2503380a00..c2cef246a9 100644 --- a/bitwarden_license/src/Scim/Groups/PutGroupCommand.cs +++ b/bitwarden_license/src/Scim/Groups/PutGroupCommand.cs @@ -1,10 +1,8 @@ using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Scim.Context; using Bit.Scim.Groups.Interfaces; using Bit.Scim.Models; @@ -13,16 +11,13 @@ namespace Bit.Scim.Groups; public class PutGroupCommand : IPutGroupCommand { private readonly IGroupRepository _groupRepository; - private readonly IScimContext _scimContext; private readonly IUpdateGroupCommand _updateGroupCommand; public PutGroupCommand( IGroupRepository groupRepository, - IScimContext scimContext, IUpdateGroupCommand updateGroupCommand) { _groupRepository = groupRepository; - _scimContext = scimContext; _updateGroupCommand = updateGroupCommand; } @@ -43,12 +38,6 @@ public class PutGroupCommand : IPutGroupCommand private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model) { - if (_scimContext.RequestScimProvider != ScimProviderType.Okta && - _scimContext.RequestScimProvider != ScimProviderType.Ping) - { - return; - } - if (model.Members == null) { return; diff --git a/bitwarden_license/test/Scim.Test/Groups/PostGroupCommandTests.cs b/bitwarden_license/test/Scim.Test/Groups/PostGroupCommandTests.cs index acf1c6c782..b44295192b 100644 --- a/bitwarden_license/test/Scim.Test/Groups/PostGroupCommandTests.cs +++ b/bitwarden_license/test/Scim.Test/Groups/PostGroupCommandTests.cs @@ -1,10 +1,8 @@ using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Scim.Context; using Bit.Scim.Groups; using Bit.Scim.Models; using Bit.Scim.Utilities; @@ -73,10 +71,6 @@ public class PostGroupCommandTests .GetManyByOrganizationIdAsync(organization.Id) .Returns(groups); - sutProvider.GetDependency() - .RequestScimProvider - .Returns(ScimProviderType.Okta); - var group = await sutProvider.Sut.PostGroupAsync(organization, scimGroupRequestModel); await sutProvider.GetDependency().Received(1).CreateGroupAsync(group, organization, EventSystemUser.SCIM, null); diff --git a/bitwarden_license/test/Scim.Test/Groups/PutGroupCommandTests.cs b/bitwarden_license/test/Scim.Test/Groups/PutGroupCommandTests.cs index 579a4b4e64..fb67cd684b 100644 --- a/bitwarden_license/test/Scim.Test/Groups/PutGroupCommandTests.cs +++ b/bitwarden_license/test/Scim.Test/Groups/PutGroupCommandTests.cs @@ -1,10 +1,8 @@ using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Scim.Context; using Bit.Scim.Groups; using Bit.Scim.Models; using Bit.Scim.Utilities; @@ -62,10 +60,6 @@ public class PutGroupCommandTests .GetByIdAsync(group.Id) .Returns(group); - sutProvider.GetDependency() - .RequestScimProvider - .Returns(ScimProviderType.Okta); - var inputModel = new ScimGroupRequestModel { DisplayName = displayName, From 0c482eea3234636062283142d000ddb2f4cf32b6 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Thu, 13 Feb 2025 14:14:55 +0100 Subject: [PATCH 130/148] PM-10600: Missing Notification Center MaxLength on Body column (#5402) --- ...8_NotificationCenterBodyLength.Designer.cs | 3010 ++++++++++++++++ ...0213120818_NotificationCenterBodyLength.cs | 41 + .../DatabaseContextModelSnapshot.cs | 3 +- ...9_NotificationCenterBodyLength.Designer.cs | 3016 +++++++++++++++++ ...0213120809_NotificationCenterBodyLength.cs | 37 + .../DatabaseContextModelSnapshot.cs | 3 +- ...4_NotificationCenterBodyLength.Designer.cs | 2999 ++++++++++++++++ ...0213120814_NotificationCenterBodyLength.cs | 21 + .../DatabaseContextModelSnapshot.cs | 1 + 9 files changed, 9129 insertions(+), 2 deletions(-) create mode 100644 util/MySqlMigrations/Migrations/20250213120818_NotificationCenterBodyLength.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250213120818_NotificationCenterBodyLength.cs create mode 100644 util/PostgresMigrations/Migrations/20250213120809_NotificationCenterBodyLength.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250213120809_NotificationCenterBodyLength.cs create mode 100644 util/SqliteMigrations/Migrations/20250213120814_NotificationCenterBodyLength.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250213120814_NotificationCenterBodyLength.cs diff --git a/util/MySqlMigrations/Migrations/20250213120818_NotificationCenterBodyLength.Designer.cs b/util/MySqlMigrations/Migrations/20250213120818_NotificationCenterBodyLength.Designer.cs new file mode 100644 index 0000000000..78771a45b7 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250213120818_NotificationCenterBodyLength.Designer.cs @@ -0,0 +1,3010 @@ +// +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("20250213120818_NotificationCenterBodyLength")] + partial class NotificationCenterBodyLength + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitItemDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseRiskInsights") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("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.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("VerifyDevices") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("varchar(3000)"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("TaskId") + .HasColumnType("char(36)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20250213120818_NotificationCenterBodyLength.cs b/util/MySqlMigrations/Migrations/20250213120818_NotificationCenterBodyLength.cs new file mode 100644 index 0000000000..47888410b1 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250213120818_NotificationCenterBodyLength.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class NotificationCenterBodyLength : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Body", + table: "Notification", + type: "varchar(3000)", + maxLength: 3000, + nullable: true, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Body", + table: "Notification", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "varchar(3000)", + oldMaxLength: 3000, + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 44ef9e01c3..73ed8e0c6b 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1654,7 +1654,8 @@ namespace Bit.MySqlMigrations.Migrations .HasColumnType("char(36)"); b.Property("Body") - .HasColumnType("longtext"); + .HasMaxLength(3000) + .HasColumnType("varchar(3000)"); b.Property("ClientType") .HasColumnType("tinyint unsigned"); diff --git a/util/PostgresMigrations/Migrations/20250213120809_NotificationCenterBodyLength.Designer.cs b/util/PostgresMigrations/Migrations/20250213120809_NotificationCenterBodyLength.Designer.cs new file mode 100644 index 0000000000..12c3821158 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250213120809_NotificationCenterBodyLength.Designer.cs @@ -0,0 +1,3016 @@ +// +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("20250213120809_NotificationCenterBodyLength")] + partial class NotificationCenterBodyLength + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("LimitItemDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseRiskInsights") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("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.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property("VerifyDevices") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20250213120809_NotificationCenterBodyLength.cs b/util/PostgresMigrations/Migrations/20250213120809_NotificationCenterBodyLength.cs new file mode 100644 index 0000000000..11aac4ef56 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250213120809_NotificationCenterBodyLength.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class NotificationCenterBodyLength : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Body", + table: "Notification", + type: "character varying(3000)", + maxLength: 3000, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Body", + table: "Notification", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(3000)", + oldMaxLength: 3000, + oldNullable: true); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 11fa811d98..a6017652bf 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1660,7 +1660,8 @@ namespace Bit.PostgresMigrations.Migrations .HasColumnType("uuid"); b.Property("Body") - .HasColumnType("text"); + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); b.Property("ClientType") .HasColumnType("smallint"); diff --git a/util/SqliteMigrations/Migrations/20250213120814_NotificationCenterBodyLength.Designer.cs b/util/SqliteMigrations/Migrations/20250213120814_NotificationCenterBodyLength.Designer.cs new file mode 100644 index 0000000000..91b7b87e88 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250213120814_NotificationCenterBodyLength.Designer.cs @@ -0,0 +1,2999 @@ +// +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("20250213120814_NotificationCenterBodyLength")] + partial class NotificationCenterBodyLength + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitItemDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseRiskInsights") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("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.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("VerifyDevices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("TaskId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/SqliteMigrations/Migrations/20250213120814_NotificationCenterBodyLength.cs b/util/SqliteMigrations/Migrations/20250213120814_NotificationCenterBodyLength.cs new file mode 100644 index 0000000000..0dac35755d --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250213120814_NotificationCenterBodyLength.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class NotificationCenterBodyLength : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 8d122f5617..185caf3074 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1643,6 +1643,7 @@ namespace Bit.SqliteMigrations.Migrations .HasColumnType("TEXT"); b.Property("Body") + .HasMaxLength(3000) .HasColumnType("TEXT"); b.Property("ClientType") From c3924bbf3bb6eb78a0339477a7ae03a95fb0d4c7 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Thu, 13 Feb 2025 14:23:33 +0100 Subject: [PATCH 131/148] [PM-10564] Push notification updates to other clients (#5057) * PM-10600: Notification push notification * PM-10600: Sending to specific client types for relay push notifications * PM-10600: Sending to specific client types for other clients * PM-10600: Send push notification on notification creation * PM-10600: Explicit group names * PM-10600: Id typos * PM-10600: Revert global push notifications * PM-10600: Added DeviceType claim * PM-10600: Sent to organization typo * PM-10600: UT coverage * PM-10600: Small refactor, UTs coverage * PM-10600: UTs coverage * PM-10600: Startup fix * PM-10600: Test fix * PM-10600: Required attribute, organization group for push notification fix * PM-10600: UT coverage * PM-10600: Fix Mobile devices not registering to organization push notifications We only register devices for organization push notifications when the organization is being created. This does not work, since we have a use case (Notification Center) of delivering notifications to all users of organization. This fixes it, by adding the organization id tag when device registers for push notifications. * PM-10600: Unit Test coverage for NotificationHubPushRegistrationService Fixed IFeatureService substitute mocking for Android tests. Added user part of organization test with organizationId tags expectation. * PM-10600: Unit Tests fix to NotificationHubPushRegistrationService after merge conflict * PM-10600: Organization push notifications not sending to mobile device from self-hosted. Self-hosted instance uses relay to register the mobile device against Bitwarden Cloud Api. Only the self-hosted server knows client's organization membership, which means it needs to pass in the organization id's information to the relay. Similarly, for Bitwarden Cloud, the organizaton id will come directly from the server. * PM-10600: Fix self-hosted organization notification not being received by mobile device. When mobile device registers on self-hosted through the relay, every single id, like user id, device id and now organization id needs to be prefixed with the installation id. This have been missing in the PushController that handles this for organization id. * 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. * PM-10600: Merge conflicts fix * merge conflict fix * PM-10600: Push notification with full notification center content. Notification Center push notification now includes all the fields. * PM-10564: Push notification updates to other clients Cherry-picked and squashed commits: d9711b6031a1bc1d96b920e521e6f37de1b434ec 6e69c8a0ce9a5ee29df9988b20c6e531c0b4e4a3 01c814595e572911574066802b661c83b116a865 3885885d5f4be39fdc2b8d258867c8a7536491cd 1285a7e994921b0e6f9ba78f9b84d8e7a6ceda2f fcf346985f367c462ef7b65ce7d5d2612f7345cc 28ff53c293f4d37de5fa40d2964f924368e13c95 57804ae27cbf25d88d148f399ce81c1c09997e10 1c9339b6869926e59076202e06341e5d4a403cc7 * null check fix * logging using template formatting --- src/Core/Enums/PushType.cs | 1 + src/Core/Models/PushNotification.cs | 13 +- .../CreateNotificationStatusCommand.cs | 12 +- .../MarkNotificationDeletedCommand.cs | 12 +- .../Commands/MarkNotificationReadCommand.cs | 12 +- .../Commands/UpdateNotificationCommand.cs | 8 +- .../NotificationHubPushNotificationService.cs | 50 +++- .../AzureQueuePushNotificationService.cs | 36 ++- .../Push/Services/IPushNotificationService.cs | 12 +- .../MultiServicePushNotificationService.cs | 33 ++- .../Services/NoopPushNotificationService.cs | 14 +- ...NotificationsApiPushNotificationService.cs | 40 +++- .../Services/RelayPushNotificationService.cs | 45 +++- src/Notifications/HubHelpers.cs | 1 + .../Commands/CreateNotificationCommandTest.cs | 9 + .../CreateNotificationStatusCommandTest.cs | 25 ++ .../MarkNotificationDeletedCommandTest.cs | 77 +++++- .../MarkNotificationReadCommandTest.cs | 77 +++++- .../Commands/UpdateNotificationCommandTest.cs | 19 ++ ...ficationHubPushNotificationServiceTests.cs | 226 +++++++++++++++--- .../AzureQueuePushNotificationServiceTests.cs | 34 ++- ...ultiServicePushNotificationServiceTests.cs | 16 ++ 22 files changed, 646 insertions(+), 126 deletions(-) diff --git a/src/Core/Enums/PushType.cs b/src/Core/Enums/PushType.cs index b656e70601..d4a4caeb9e 100644 --- a/src/Core/Enums/PushType.cs +++ b/src/Core/Enums/PushType.cs @@ -29,4 +29,5 @@ public enum PushType : byte SyncOrganizationCollectionSettingChanged = 19, SyncNotification = 20, + SyncNotificationStatus = 21 } diff --git a/src/Core/Models/PushNotification.cs b/src/Core/Models/PushNotification.cs index a6e8852e95..775c3443f2 100644 --- a/src/Core/Models/PushNotification.cs +++ b/src/Core/Models/PushNotification.cs @@ -1,11 +1,12 @@ -using Bit.Core.Enums; +#nullable enable +using Bit.Core.Enums; using Bit.Core.NotificationCenter.Enums; namespace Bit.Core.Models; public class PushNotificationData { - public PushNotificationData(PushType type, T payload, string contextId) + public PushNotificationData(PushType type, T payload, string? contextId) { Type = type; Payload = payload; @@ -14,7 +15,7 @@ public class PushNotificationData public PushType Type { get; set; } public T Payload { get; set; } - public string ContextId { get; set; } + public string? ContextId { get; set; } } public class SyncCipherPushNotification @@ -22,7 +23,7 @@ public class SyncCipherPushNotification public Guid Id { get; set; } public Guid? UserId { get; set; } public Guid? OrganizationId { get; set; } - public IEnumerable CollectionIds { get; set; } + public IEnumerable? CollectionIds { get; set; } public DateTime RevisionDate { get; set; } } @@ -46,7 +47,6 @@ public class SyncSendPushNotification public DateTime RevisionDate { get; set; } } -#nullable enable public class NotificationPushNotification { public Guid Id { get; set; } @@ -59,8 +59,9 @@ public class NotificationPushNotification public string? Body { get; set; } public DateTime CreationDate { get; set; } public DateTime RevisionDate { get; set; } + public DateTime? ReadDate { get; set; } + public DateTime? DeletedDate { get; set; } } -#nullable disable public class AuthRequestPushNotification { diff --git a/src/Core/NotificationCenter/Commands/CreateNotificationStatusCommand.cs b/src/Core/NotificationCenter/Commands/CreateNotificationStatusCommand.cs index fcd61ceebc..793da22f81 100644 --- a/src/Core/NotificationCenter/Commands/CreateNotificationStatusCommand.cs +++ b/src/Core/NotificationCenter/Commands/CreateNotificationStatusCommand.cs @@ -5,6 +5,7 @@ using Bit.Core.NotificationCenter.Authorization; using Bit.Core.NotificationCenter.Commands.Interfaces; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Platform.Push; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; @@ -16,16 +17,19 @@ public class CreateNotificationStatusCommand : ICreateNotificationStatusCommand private readonly IAuthorizationService _authorizationService; private readonly INotificationRepository _notificationRepository; private readonly INotificationStatusRepository _notificationStatusRepository; + private readonly IPushNotificationService _pushNotificationService; public CreateNotificationStatusCommand(ICurrentContext currentContext, IAuthorizationService authorizationService, INotificationRepository notificationRepository, - INotificationStatusRepository notificationStatusRepository) + INotificationStatusRepository notificationStatusRepository, + IPushNotificationService pushNotificationService) { _currentContext = currentContext; _authorizationService = authorizationService; _notificationRepository = notificationRepository; _notificationStatusRepository = notificationStatusRepository; + _pushNotificationService = pushNotificationService; } public async Task CreateAsync(NotificationStatus notificationStatus) @@ -42,6 +46,10 @@ public class CreateNotificationStatusCommand : ICreateNotificationStatusCommand await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus, NotificationStatusOperations.Create); - return await _notificationStatusRepository.CreateAsync(notificationStatus); + var newNotificationStatus = await _notificationStatusRepository.CreateAsync(notificationStatus); + + await _pushNotificationService.PushNotificationStatusAsync(notification, newNotificationStatus); + + return newNotificationStatus; } } diff --git a/src/Core/NotificationCenter/Commands/MarkNotificationDeletedCommand.cs b/src/Core/NotificationCenter/Commands/MarkNotificationDeletedCommand.cs index 2ca7aa9051..256702c10c 100644 --- a/src/Core/NotificationCenter/Commands/MarkNotificationDeletedCommand.cs +++ b/src/Core/NotificationCenter/Commands/MarkNotificationDeletedCommand.cs @@ -5,6 +5,7 @@ using Bit.Core.NotificationCenter.Authorization; using Bit.Core.NotificationCenter.Commands.Interfaces; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Platform.Push; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; @@ -16,16 +17,19 @@ public class MarkNotificationDeletedCommand : IMarkNotificationDeletedCommand private readonly IAuthorizationService _authorizationService; private readonly INotificationRepository _notificationRepository; private readonly INotificationStatusRepository _notificationStatusRepository; + private readonly IPushNotificationService _pushNotificationService; public MarkNotificationDeletedCommand(ICurrentContext currentContext, IAuthorizationService authorizationService, INotificationRepository notificationRepository, - INotificationStatusRepository notificationStatusRepository) + INotificationStatusRepository notificationStatusRepository, + IPushNotificationService pushNotificationService) { _currentContext = currentContext; _authorizationService = authorizationService; _notificationRepository = notificationRepository; _notificationStatusRepository = notificationStatusRepository; + _pushNotificationService = pushNotificationService; } public async Task MarkDeletedAsync(Guid notificationId) @@ -59,7 +63,9 @@ public class MarkNotificationDeletedCommand : IMarkNotificationDeletedCommand await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus, NotificationStatusOperations.Create); - await _notificationStatusRepository.CreateAsync(notificationStatus); + var newNotificationStatus = await _notificationStatusRepository.CreateAsync(notificationStatus); + + await _pushNotificationService.PushNotificationStatusAsync(notification, newNotificationStatus); } else { @@ -69,6 +75,8 @@ public class MarkNotificationDeletedCommand : IMarkNotificationDeletedCommand notificationStatus.DeletedDate = DateTime.UtcNow; await _notificationStatusRepository.UpdateAsync(notificationStatus); + + await _pushNotificationService.PushNotificationStatusAsync(notification, notificationStatus); } } } diff --git a/src/Core/NotificationCenter/Commands/MarkNotificationReadCommand.cs b/src/Core/NotificationCenter/Commands/MarkNotificationReadCommand.cs index 400e44463a..9c9d1d48a2 100644 --- a/src/Core/NotificationCenter/Commands/MarkNotificationReadCommand.cs +++ b/src/Core/NotificationCenter/Commands/MarkNotificationReadCommand.cs @@ -5,6 +5,7 @@ using Bit.Core.NotificationCenter.Authorization; using Bit.Core.NotificationCenter.Commands.Interfaces; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Platform.Push; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; @@ -16,16 +17,19 @@ public class MarkNotificationReadCommand : IMarkNotificationReadCommand private readonly IAuthorizationService _authorizationService; private readonly INotificationRepository _notificationRepository; private readonly INotificationStatusRepository _notificationStatusRepository; + private readonly IPushNotificationService _pushNotificationService; public MarkNotificationReadCommand(ICurrentContext currentContext, IAuthorizationService authorizationService, INotificationRepository notificationRepository, - INotificationStatusRepository notificationStatusRepository) + INotificationStatusRepository notificationStatusRepository, + IPushNotificationService pushNotificationService) { _currentContext = currentContext; _authorizationService = authorizationService; _notificationRepository = notificationRepository; _notificationStatusRepository = notificationStatusRepository; + _pushNotificationService = pushNotificationService; } public async Task MarkReadAsync(Guid notificationId) @@ -59,7 +63,9 @@ public class MarkNotificationReadCommand : IMarkNotificationReadCommand await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus, NotificationStatusOperations.Create); - await _notificationStatusRepository.CreateAsync(notificationStatus); + var newNotificationStatus = await _notificationStatusRepository.CreateAsync(notificationStatus); + + await _pushNotificationService.PushNotificationStatusAsync(notification, newNotificationStatus); } else { @@ -69,6 +75,8 @@ public class MarkNotificationReadCommand : IMarkNotificationReadCommand notificationStatus.ReadDate = DateTime.UtcNow; await _notificationStatusRepository.UpdateAsync(notificationStatus); + + await _pushNotificationService.PushNotificationStatusAsync(notification, notificationStatus); } } } diff --git a/src/Core/NotificationCenter/Commands/UpdateNotificationCommand.cs b/src/Core/NotificationCenter/Commands/UpdateNotificationCommand.cs index f049478178..471786aac6 100644 --- a/src/Core/NotificationCenter/Commands/UpdateNotificationCommand.cs +++ b/src/Core/NotificationCenter/Commands/UpdateNotificationCommand.cs @@ -5,6 +5,7 @@ using Bit.Core.NotificationCenter.Authorization; using Bit.Core.NotificationCenter.Commands.Interfaces; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Platform.Push; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; @@ -15,14 +16,17 @@ public class UpdateNotificationCommand : IUpdateNotificationCommand private readonly ICurrentContext _currentContext; private readonly IAuthorizationService _authorizationService; private readonly INotificationRepository _notificationRepository; + private readonly IPushNotificationService _pushNotificationService; public UpdateNotificationCommand(ICurrentContext currentContext, IAuthorizationService authorizationService, - INotificationRepository notificationRepository) + INotificationRepository notificationRepository, + IPushNotificationService pushNotificationService) { _currentContext = currentContext; _authorizationService = authorizationService; _notificationRepository = notificationRepository; + _pushNotificationService = pushNotificationService; } public async Task UpdateAsync(Notification notificationToUpdate) @@ -43,5 +47,7 @@ public class UpdateNotificationCommand : IUpdateNotificationCommand notification.RevisionDate = DateTime.UtcNow; await _notificationRepository.ReplaceAsync(notification); + + await _pushNotificationService.PushNotificationAsync(notification); } } diff --git a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs index d1c7749d9f..7baf0352ee 100644 --- a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +#nullable enable +using System.Text.Json; using System.Text.RegularExpressions; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; @@ -6,6 +7,7 @@ using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models; using Bit.Core.Models.Data; +using Bit.Core.NotificationCenter.Entities; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Tools.Entities; @@ -51,7 +53,7 @@ public class NotificationHubPushNotificationService : IPushNotificationService await PushCipherAsync(cipher, PushType.SyncLoginDelete, null); } - private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable collectionIds) + private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable? collectionIds) { if (cipher.OrganizationId.HasValue) { @@ -209,6 +211,36 @@ public class NotificationHubPushNotificationService : IPushNotificationService } } + public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) + { + var message = new NotificationPushNotification + { + Id = notification.Id, + Priority = notification.Priority, + Global = notification.Global, + ClientType = notification.ClientType, + UserId = notification.UserId, + OrganizationId = notification.OrganizationId, + Title = notification.Title, + Body = notification.Body, + CreationDate = notification.CreationDate, + RevisionDate = notification.RevisionDate, + ReadDate = notificationStatus.ReadDate, + DeletedDate = notificationStatus.DeletedDate + }; + + if (notification.UserId.HasValue) + { + await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotificationStatus, message, true, + notification.ClientType); + } + else if (notification.OrganizationId.HasValue) + { + await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotificationStatus, message, + true, notification.ClientType); + } + } + private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type) { var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId }; @@ -230,8 +262,8 @@ public class NotificationHubPushNotificationService : IPushNotificationService GetContextIdentifier(excludeCurrentContext), clientType: clientType); } - public async Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, - string deviceId = null, ClientType? clientType = null) + public async Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) { var tag = BuildTag($"template:payload_userId:{SanitizeTagInput(userId)}", identifier, clientType); await SendPayloadAsync(tag, type, payload); @@ -241,8 +273,8 @@ public class NotificationHubPushNotificationService : IPushNotificationService } } - public async Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier, - string deviceId = null, ClientType? clientType = null) + public async Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) { var tag = BuildTag($"template:payload && organizationId:{SanitizeTagInput(orgId)}", identifier, clientType); await SendPayloadAsync(tag, type, payload); @@ -277,7 +309,7 @@ public class NotificationHubPushNotificationService : IPushNotificationService false ); - private string GetContextIdentifier(bool excludeCurrentContext) + private string? GetContextIdentifier(bool excludeCurrentContext) { if (!excludeCurrentContext) { @@ -285,11 +317,11 @@ public class NotificationHubPushNotificationService : IPushNotificationService } var currentContext = - _httpContextAccessor?.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext; + _httpContextAccessor.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext; return currentContext?.DeviceIdentifier; } - private string BuildTag(string tag, string identifier, ClientType? clientType) + private string BuildTag(string tag, string? identifier, ClientType? clientType) { if (!string.IsNullOrWhiteSpace(identifier)) { diff --git a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs index a25a017192..c32212c6b2 100644 --- a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs +++ b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +#nullable enable +using System.Text.Json; using Azure.Storage.Queues; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; @@ -42,7 +43,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService await PushCipherAsync(cipher, PushType.SyncLoginDelete, null); } - private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable collectionIds) + private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable? collectionIds) { if (cipher.OrganizationId.HasValue) { @@ -184,6 +185,27 @@ public class AzureQueuePushNotificationService : IPushNotificationService await SendMessageAsync(PushType.SyncNotification, message, true); } + public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) + { + var message = new NotificationPushNotification + { + Id = notification.Id, + Priority = notification.Priority, + Global = notification.Global, + ClientType = notification.ClientType, + UserId = notification.UserId, + OrganizationId = notification.OrganizationId, + Title = notification.Title, + Body = notification.Body, + CreationDate = notification.CreationDate, + RevisionDate = notification.RevisionDate, + ReadDate = notificationStatus.ReadDate, + DeletedDate = notificationStatus.DeletedDate + }; + + await SendMessageAsync(PushType.SyncNotificationStatus, message, true); + } + private async Task PushSendAsync(Send send, PushType type) { if (send.UserId.HasValue) @@ -207,7 +229,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService await _queueClient.SendMessageAsync(message); } - private string GetContextIdentifier(bool excludeCurrentContext) + private string? GetContextIdentifier(bool excludeCurrentContext) { if (!excludeCurrentContext) { @@ -219,15 +241,15 @@ public class AzureQueuePushNotificationService : IPushNotificationService return currentContext?.DeviceIdentifier; } - public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, - string deviceId = null, ClientType? clientType = null) + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) { // Noop return Task.FromResult(0); } - public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier, - string deviceId = null, ClientType? clientType = null) + public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) { // Noop return Task.FromResult(0); diff --git a/src/Core/Platform/Push/Services/IPushNotificationService.cs b/src/Core/Platform/Push/Services/IPushNotificationService.cs index 7f2b6c90fe..1c7fdc659b 100644 --- a/src/Core/Platform/Push/Services/IPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/IPushNotificationService.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Entities; +#nullable enable +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Enums; using Bit.Core.NotificationCenter.Entities; @@ -25,12 +26,13 @@ public interface IPushNotificationService Task PushSyncSendUpdateAsync(Send send); Task PushSyncSendDeleteAsync(Send send); Task PushNotificationAsync(Notification notification); + Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus); Task PushAuthRequestAsync(AuthRequest authRequest); Task PushAuthRequestResponseAsync(AuthRequest authRequest); Task PushSyncOrganizationStatusAsync(Organization organization); Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization); - Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, - string deviceId = null, ClientType? clientType = null); - Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier, - string deviceId = null, ClientType? clientType = null); + Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null); + Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null); } diff --git a/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs b/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs index db3107b6c3..9b4e66ae1a 100644 --- a/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs +++ b/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Entities; +#nullable enable +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Enums; using Bit.Core.NotificationCenter.Entities; @@ -24,7 +25,7 @@ public class MultiServicePushNotificationService : IPushNotificationService _logger = logger; _logger.LogInformation("Hub services: {Services}", _services.Count()); - globalSettings?.NotificationHubPool?.NotificationHubs?.ForEach(hub => + globalSettings.NotificationHubPool?.NotificationHubs?.ForEach(hub => { _logger.LogInformation("HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}", hub.HubName, hub.EnableSendTracing, hub.RegistrationStartDate, hub.RegistrationEndDate); }); @@ -150,15 +151,21 @@ public class MultiServicePushNotificationService : IPushNotificationService return Task.CompletedTask; } - public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, - string deviceId = null, ClientType? clientType = null) + public Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) + { + PushToServices((s) => s.PushNotificationStatusAsync(notification, notificationStatus)); + return Task.CompletedTask; + } + + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) { PushToServices((s) => s.SendPayloadToUserAsync(userId, type, payload, identifier, deviceId, clientType)); return Task.FromResult(0); } - public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier, - string deviceId = null, ClientType? clientType = null) + public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) { PushToServices((s) => s.SendPayloadToOrganizationAsync(orgId, type, payload, identifier, deviceId, clientType)); return Task.FromResult(0); @@ -166,12 +173,16 @@ public class MultiServicePushNotificationService : IPushNotificationService private void PushToServices(Func pushFunc) { - if (_services != null) + if (!_services.Any()) { - foreach (var service in _services) - { - pushFunc(service); - } + _logger.LogWarning("No services found to push notification"); + return; + } + + foreach (var service in _services) + { + _logger.LogDebug("Pushing notification to service {ServiceName}", service.GetType().Name); + pushFunc(service); } } } diff --git a/src/Core/Platform/Push/Services/NoopPushNotificationService.cs b/src/Core/Platform/Push/Services/NoopPushNotificationService.cs index fb4121179f..57c446c5e5 100644 --- a/src/Core/Platform/Push/Services/NoopPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/NoopPushNotificationService.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Entities; +#nullable enable +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Enums; using Bit.Core.NotificationCenter.Entities; @@ -84,8 +85,8 @@ public class NoopPushNotificationService : IPushNotificationService return Task.FromResult(0); } - public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier, - string deviceId = null, ClientType? clientType = null) + public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) { return Task.FromResult(0); } @@ -107,11 +108,14 @@ public class NoopPushNotificationService : IPushNotificationService return Task.FromResult(0); } - public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, - string deviceId = null, ClientType? clientType = null) + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) { return Task.FromResult(0); } public Task PushNotificationAsync(Notification notification) => Task.CompletedTask; + + public Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) => + Task.CompletedTask; } diff --git a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs index fb3814fd64..7a557e8978 100644 --- a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Entities; +#nullable enable +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Context; using Bit.Core.Enums; @@ -16,7 +17,6 @@ namespace Bit.Core.Platform.Push; public class NotificationsApiPushNotificationService : BaseIdentityClientService, IPushNotificationService { - private readonly GlobalSettings _globalSettings; private readonly IHttpContextAccessor _httpContextAccessor; public NotificationsApiPushNotificationService( @@ -33,7 +33,6 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService globalSettings.InternalIdentityKey, logger) { - _globalSettings = globalSettings; _httpContextAccessor = httpContextAccessor; } @@ -52,7 +51,7 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService await PushCipherAsync(cipher, PushType.SyncLoginDelete, null); } - private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable collectionIds) + private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable? collectionIds) { if (cipher.OrganizationId.HasValue) { @@ -203,6 +202,27 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService await SendMessageAsync(PushType.SyncNotification, message, true); } + public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) + { + var message = new NotificationPushNotification + { + Id = notification.Id, + Priority = notification.Priority, + Global = notification.Global, + ClientType = notification.ClientType, + UserId = notification.UserId, + OrganizationId = notification.OrganizationId, + Title = notification.Title, + Body = notification.Body, + CreationDate = notification.CreationDate, + RevisionDate = notification.RevisionDate, + ReadDate = notificationStatus.ReadDate, + DeletedDate = notificationStatus.DeletedDate + }; + + await SendMessageAsync(PushType.SyncNotificationStatus, message, true); + } + private async Task PushSendAsync(Send send, PushType type) { if (send.UserId.HasValue) @@ -225,7 +245,7 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService await SendAsync(HttpMethod.Post, "send", request); } - private string GetContextIdentifier(bool excludeCurrentContext) + private string? GetContextIdentifier(bool excludeCurrentContext) { if (!excludeCurrentContext) { @@ -233,19 +253,19 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService } var currentContext = - _httpContextAccessor?.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext; + _httpContextAccessor.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext; return currentContext?.DeviceIdentifier; } - public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, - string deviceId = null, ClientType? clientType = null) + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) { // Noop return Task.FromResult(0); } - public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier, - string deviceId = null, ClientType? clientType = null) + public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) { // Noop return Task.FromResult(0); diff --git a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs index 4d99f04768..09f42fd0d1 100644 --- a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Entities; +#nullable enable +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Context; using Bit.Core.Enums; @@ -55,7 +56,7 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti await PushCipherAsync(cipher, PushType.SyncLoginDelete, null); } - private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable collectionIds) + private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable? collectionIds) { if (cipher.OrganizationId.HasValue) { @@ -219,6 +220,36 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti } } + public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) + { + var message = new NotificationPushNotification + { + Id = notification.Id, + Priority = notification.Priority, + Global = notification.Global, + ClientType = notification.ClientType, + UserId = notification.UserId, + OrganizationId = notification.OrganizationId, + Title = notification.Title, + Body = notification.Body, + CreationDate = notification.CreationDate, + RevisionDate = notification.RevisionDate, + ReadDate = notificationStatus.ReadDate, + DeletedDate = notificationStatus.DeletedDate + }; + + if (notification.UserId.HasValue) + { + await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotificationStatus, message, true, + notification.ClientType); + } + else if (notification.OrganizationId.HasValue) + { + await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotificationStatus, message, + true, notification.ClientType); + } + } + public async Task PushSyncOrganizationStatusAsync(Organization organization) { var message = new OrganizationStatusPushNotification @@ -277,7 +308,7 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti private async Task AddCurrentContextAsync(PushSendRequestModel request, bool addIdentifier) { var currentContext = - _httpContextAccessor?.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext; + _httpContextAccessor.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext; if (!string.IsNullOrWhiteSpace(currentContext?.DeviceIdentifier)) { var device = await _deviceRepository.GetByIdentifierAsync(currentContext.DeviceIdentifier); @@ -293,14 +324,14 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti } } - public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, - string deviceId = null, ClientType? clientType = null) + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) { throw new NotImplementedException(); } - public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier, - string deviceId = null, ClientType? clientType = null) + public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) { throw new NotImplementedException(); } diff --git a/src/Notifications/HubHelpers.cs b/src/Notifications/HubHelpers.cs index 9b0164fdc5..af571e48c4 100644 --- a/src/Notifications/HubHelpers.cs +++ b/src/Notifications/HubHelpers.cs @@ -104,6 +104,7 @@ public static class HubHelpers .SendAsync("ReceiveMessage", organizationCollectionSettingsChangedNotification, cancellationToken); break; case PushType.SyncNotification: + case PushType.SyncNotificationStatus: var syncNotification = JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); diff --git a/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs index 41efce82ab..3256f2f9cb 100644 --- a/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs +++ b/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs @@ -41,6 +41,12 @@ public class CreateNotificationCommandTest Setup(sutProvider, notification, authorized: false); await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(notification)); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); } [Theory] @@ -59,5 +65,8 @@ public class CreateNotificationCommandTest await sutProvider.GetDependency() .Received(1) .PushNotificationAsync(newNotification); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); } } diff --git a/test/Core.Test/NotificationCenter/Commands/CreateNotificationStatusCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/CreateNotificationStatusCommandTest.cs index 8dc8524926..78aaaba18f 100644 --- a/test/Core.Test/NotificationCenter/Commands/CreateNotificationStatusCommandTest.cs +++ b/test/Core.Test/NotificationCenter/Commands/CreateNotificationStatusCommandTest.cs @@ -5,6 +5,7 @@ using Bit.Core.NotificationCenter.Authorization; using Bit.Core.NotificationCenter.Commands; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Platform.Push; using Bit.Core.Test.NotificationCenter.AutoFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -50,6 +51,12 @@ public class CreateNotificationStatusCommandTest Setup(sutProvider, notification: null, notificationStatus, true, true); await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(notificationStatus)); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); } [Theory] @@ -61,6 +68,12 @@ public class CreateNotificationStatusCommandTest Setup(sutProvider, notification, notificationStatus, authorizedNotification: false, true); await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(notificationStatus)); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); } [Theory] @@ -72,6 +85,12 @@ public class CreateNotificationStatusCommandTest Setup(sutProvider, notification, notificationStatus, true, authorizedCreate: false); await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(notificationStatus)); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); } [Theory] @@ -85,5 +104,11 @@ public class CreateNotificationStatusCommandTest var newNotificationStatus = await sutProvider.Sut.CreateAsync(notificationStatus); Assert.Equal(notificationStatus, newNotificationStatus); + await sutProvider.GetDependency() + .Received(1) + .PushNotificationStatusAsync(notification, notificationStatus); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); } } diff --git a/test/Core.Test/NotificationCenter/Commands/MarkNotificationDeletedCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/MarkNotificationDeletedCommandTest.cs index a5bb20423c..f1d23b5f18 100644 --- a/test/Core.Test/NotificationCenter/Commands/MarkNotificationDeletedCommandTest.cs +++ b/test/Core.Test/NotificationCenter/Commands/MarkNotificationDeletedCommandTest.cs @@ -6,6 +6,7 @@ using Bit.Core.NotificationCenter.Authorization; using Bit.Core.NotificationCenter.Commands; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Platform.Push; using Bit.Core.Test.NotificationCenter.AutoFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -63,6 +64,12 @@ public class MarkNotificationDeletedCommandTest Setup(sutProvider, notificationId, userId: null, notification, notificationStatus, true, true, true); await Assert.ThrowsAsync(() => sutProvider.Sut.MarkDeletedAsync(notificationId)); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); } [Theory] @@ -74,6 +81,12 @@ public class MarkNotificationDeletedCommandTest Setup(sutProvider, notificationId, userId, notification: null, notificationStatus, true, true, true); await Assert.ThrowsAsync(() => sutProvider.Sut.MarkDeletedAsync(notificationId)); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); } [Theory] @@ -86,6 +99,12 @@ public class MarkNotificationDeletedCommandTest true, true); await Assert.ThrowsAsync(() => sutProvider.Sut.MarkDeletedAsync(notificationId)); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); } [Theory] @@ -98,6 +117,12 @@ public class MarkNotificationDeletedCommandTest authorizedCreate: false, true); await Assert.ThrowsAsync(() => sutProvider.Sut.MarkDeletedAsync(notificationId)); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); } [Theory] @@ -110,6 +135,12 @@ public class MarkNotificationDeletedCommandTest authorizedUpdate: false); await Assert.ThrowsAsync(() => sutProvider.Sut.MarkDeletedAsync(notificationId)); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); } [Theory] @@ -119,13 +150,25 @@ public class MarkNotificationDeletedCommandTest Guid notificationId, Guid userId, Notification notification) { Setup(sutProvider, notificationId, userId, notification, notificationStatus: null, true, true, true); + var expectedNotificationStatus = new NotificationStatus + { + NotificationId = notificationId, + UserId = userId, + ReadDate = null, + DeletedDate = DateTime.UtcNow + }; await sutProvider.Sut.MarkDeletedAsync(notificationId); await sutProvider.GetDependency().Received(1) - .CreateAsync(Arg.Is(ns => - ns.NotificationId == notificationId && ns.UserId == userId && !ns.ReadDate.HasValue && - ns.DeletedDate.HasValue && DateTime.UtcNow - ns.DeletedDate.Value < TimeSpan.FromMinutes(1))); + .CreateAsync(Arg.Do(ns => AssertNotificationStatus(expectedNotificationStatus, ns))); + await sutProvider.GetDependency() + .Received(1) + .PushNotificationStatusAsync(notification, + Arg.Do(ns => AssertNotificationStatus(expectedNotificationStatus, ns))); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); } [Theory] @@ -134,18 +177,30 @@ public class MarkNotificationDeletedCommandTest SutProvider sutProvider, Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus) { - var deletedDate = notificationStatus.DeletedDate; - Setup(sutProvider, notificationId, userId, notification, notificationStatus, true, true, true); await sutProvider.Sut.MarkDeletedAsync(notificationId); await sutProvider.GetDependency().Received(1) - .UpdateAsync(Arg.Is(ns => - ns.Equals(notificationStatus) && - ns.NotificationId == notificationStatus.NotificationId && ns.UserId == notificationStatus.UserId && - ns.ReadDate == notificationStatus.ReadDate && ns.DeletedDate != deletedDate && - ns.DeletedDate.HasValue && - DateTime.UtcNow - ns.DeletedDate.Value < TimeSpan.FromMinutes(1))); + .UpdateAsync(Arg.Do(ns => AssertNotificationStatus(notificationStatus, ns))); + await sutProvider.GetDependency() + .Received(1) + .PushNotificationStatusAsync(notification, + Arg.Do(ns => AssertNotificationStatus(notificationStatus, ns))); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); + } + + private static void AssertNotificationStatus(NotificationStatus expectedNotificationStatus, + NotificationStatus? actualNotificationStatus) + { + Assert.NotNull(actualNotificationStatus); + Assert.Equal(expectedNotificationStatus.NotificationId, actualNotificationStatus.NotificationId); + Assert.Equal(expectedNotificationStatus.UserId, actualNotificationStatus.UserId); + Assert.Equal(expectedNotificationStatus.ReadDate, actualNotificationStatus.ReadDate); + Assert.NotEqual(expectedNotificationStatus.DeletedDate, actualNotificationStatus.DeletedDate); + Assert.NotNull(actualNotificationStatus.DeletedDate); + Assert.Equal(DateTime.UtcNow, actualNotificationStatus.DeletedDate.Value, TimeSpan.FromMinutes(1)); } } diff --git a/test/Core.Test/NotificationCenter/Commands/MarkNotificationReadCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/MarkNotificationReadCommandTest.cs index f80234c075..481a973d32 100644 --- a/test/Core.Test/NotificationCenter/Commands/MarkNotificationReadCommandTest.cs +++ b/test/Core.Test/NotificationCenter/Commands/MarkNotificationReadCommandTest.cs @@ -6,6 +6,7 @@ using Bit.Core.NotificationCenter.Authorization; using Bit.Core.NotificationCenter.Commands; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Platform.Push; using Bit.Core.Test.NotificationCenter.AutoFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -63,6 +64,12 @@ public class MarkNotificationReadCommandTest Setup(sutProvider, notificationId, userId: null, notification, notificationStatus, true, true, true); await Assert.ThrowsAsync(() => sutProvider.Sut.MarkReadAsync(notificationId)); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); } [Theory] @@ -74,6 +81,12 @@ public class MarkNotificationReadCommandTest Setup(sutProvider, notificationId, userId, notification: null, notificationStatus, true, true, true); await Assert.ThrowsAsync(() => sutProvider.Sut.MarkReadAsync(notificationId)); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); } [Theory] @@ -86,6 +99,12 @@ public class MarkNotificationReadCommandTest true, true); await Assert.ThrowsAsync(() => sutProvider.Sut.MarkReadAsync(notificationId)); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); } [Theory] @@ -98,6 +117,12 @@ public class MarkNotificationReadCommandTest authorizedCreate: false, true); await Assert.ThrowsAsync(() => sutProvider.Sut.MarkReadAsync(notificationId)); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); } [Theory] @@ -110,6 +135,12 @@ public class MarkNotificationReadCommandTest authorizedUpdate: false); await Assert.ThrowsAsync(() => sutProvider.Sut.MarkReadAsync(notificationId)); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); } [Theory] @@ -119,13 +150,25 @@ public class MarkNotificationReadCommandTest Guid notificationId, Guid userId, Notification notification) { Setup(sutProvider, notificationId, userId, notification, notificationStatus: null, true, true, true); + var expectedNotificationStatus = new NotificationStatus + { + NotificationId = notificationId, + UserId = userId, + ReadDate = DateTime.UtcNow, + DeletedDate = null + }; await sutProvider.Sut.MarkReadAsync(notificationId); await sutProvider.GetDependency().Received(1) - .CreateAsync(Arg.Is(ns => - ns.NotificationId == notificationId && ns.UserId == userId && !ns.DeletedDate.HasValue && - ns.ReadDate.HasValue && DateTime.UtcNow - ns.ReadDate.Value < TimeSpan.FromMinutes(1))); + .CreateAsync(Arg.Do(ns => AssertNotificationStatus(expectedNotificationStatus, ns))); + await sutProvider.GetDependency() + .Received(1) + .PushNotificationStatusAsync(notification, + Arg.Do(ns => AssertNotificationStatus(expectedNotificationStatus, ns))); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); } [Theory] @@ -134,18 +177,30 @@ public class MarkNotificationReadCommandTest SutProvider sutProvider, Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus) { - var readDate = notificationStatus.ReadDate; - Setup(sutProvider, notificationId, userId, notification, notificationStatus, true, true, true); await sutProvider.Sut.MarkReadAsync(notificationId); await sutProvider.GetDependency().Received(1) - .UpdateAsync(Arg.Is(ns => - ns.Equals(notificationStatus) && - ns.NotificationId == notificationStatus.NotificationId && ns.UserId == notificationStatus.UserId && - ns.DeletedDate == notificationStatus.DeletedDate && ns.ReadDate != readDate && - ns.ReadDate.HasValue && - DateTime.UtcNow - ns.ReadDate.Value < TimeSpan.FromMinutes(1))); + .UpdateAsync(Arg.Do(ns => AssertNotificationStatus(notificationStatus, ns))); + await sutProvider.GetDependency() + .Received(1) + .PushNotificationStatusAsync(notification, + Arg.Do(ns => AssertNotificationStatus(notificationStatus, ns))); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); + } + + private static void AssertNotificationStatus(NotificationStatus expectedNotificationStatus, + NotificationStatus? actualNotificationStatus) + { + Assert.NotNull(actualNotificationStatus); + Assert.Equal(expectedNotificationStatus.NotificationId, actualNotificationStatus.NotificationId); + Assert.Equal(expectedNotificationStatus.UserId, actualNotificationStatus.UserId); + Assert.NotEqual(expectedNotificationStatus.ReadDate, actualNotificationStatus.ReadDate); + Assert.NotNull(actualNotificationStatus.ReadDate); + Assert.Equal(DateTime.UtcNow, actualNotificationStatus.ReadDate.Value, TimeSpan.FromMinutes(1)); + Assert.Equal(expectedNotificationStatus.DeletedDate, actualNotificationStatus.DeletedDate); } } diff --git a/test/Core.Test/NotificationCenter/Commands/UpdateNotificationCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/UpdateNotificationCommandTest.cs index 976d1d77a3..406347e0df 100644 --- a/test/Core.Test/NotificationCenter/Commands/UpdateNotificationCommandTest.cs +++ b/test/Core.Test/NotificationCenter/Commands/UpdateNotificationCommandTest.cs @@ -7,6 +7,7 @@ using Bit.Core.NotificationCenter.Commands; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Enums; using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Platform.Push; using Bit.Core.Test.NotificationCenter.AutoFixture; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; @@ -45,6 +46,12 @@ public class UpdateNotificationCommandTest Setup(sutProvider, notification.Id, notification: null, true); await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(notification)); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); } [Theory] @@ -56,6 +63,12 @@ public class UpdateNotificationCommandTest Setup(sutProvider, notification.Id, notification, authorized: false); await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(notification)); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); } [Theory] @@ -91,5 +104,11 @@ public class UpdateNotificationCommandTest n.Priority == notificationToUpdate.Priority && n.ClientType == notificationToUpdate.ClientType && n.Title == notificationToUpdate.Title && n.Body == notificationToUpdate.Body && DateTime.UtcNow - n.RevisionDate < TimeSpan.FromMinutes(1))); + await sutProvider.GetDependency() + .Received(1) + .PushNotificationAsync(notification); + await sutProvider.GetDependency() + .Received(0) + .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); } } diff --git a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs index f1cfdc9f85..2b8ff88dc1 100644 --- a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs +++ b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs @@ -15,12 +15,13 @@ using Xunit; namespace Bit.Core.Test.NotificationHub; [SutProviderCustomize] +[NotificationStatusCustomize] public class NotificationHubPushNotificationServiceTests { [Theory] [BitAutoData] [NotificationCustomize] - public async void PushNotificationAsync_Global_NotSent( + public async Task PushNotificationAsync_Global_NotSent( SutProvider sutProvider, Notification notification) { await sutProvider.Sut.PushNotificationAsync(notification); @@ -39,7 +40,7 @@ public class NotificationHubPushNotificationServiceTests [BitAutoData(false)] [BitAutoData(true)] [NotificationCustomize(false)] - public async void PushNotificationAsync_UserIdProvidedClientTypeAll_SentToUser( + public async Task PushNotificationAsync_UserIdProvidedClientTypeAll_SentToUser( bool organizationIdNull, SutProvider sutProvider, Notification notification) { @@ -49,11 +50,12 @@ public class NotificationHubPushNotificationServiceTests } notification.ClientType = ClientType.All; - var expectedSyncNotification = ToSyncNotificationPushNotification(notification); + var expectedNotification = ToNotificationPushNotification(notification, null); await sutProvider.Sut.PushNotificationAsync(notification); - await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, expectedSyncNotification, + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, + expectedNotification, $"(template:payload_userId:{notification.UserId})"); await sutProvider.GetDependency() .Received(0) @@ -61,30 +63,46 @@ public class NotificationHubPushNotificationServiceTests } [Theory] - [BitAutoData(false, ClientType.Browser)] - [BitAutoData(false, ClientType.Desktop)] - [BitAutoData(false, ClientType.Web)] - [BitAutoData(false, ClientType.Mobile)] - [BitAutoData(true, ClientType.Browser)] - [BitAutoData(true, ClientType.Desktop)] - [BitAutoData(true, ClientType.Web)] - [BitAutoData(true, ClientType.Mobile)] + [BitAutoData(ClientType.Browser)] + [BitAutoData(ClientType.Desktop)] + [BitAutoData(ClientType.Web)] + [BitAutoData(ClientType.Mobile)] [NotificationCustomize(false)] - public async void PushNotificationAsync_UserIdProvidedClientTypeNotAll_SentToUser(bool organizationIdNull, + public async Task PushNotificationAsync_UserIdProvidedOrganizationIdNullClientTypeNotAll_SentToUser( ClientType clientType, SutProvider sutProvider, Notification notification) { - if (organizationIdNull) - { - notification.OrganizationId = null; - } - + notification.OrganizationId = null; notification.ClientType = clientType; - var expectedSyncNotification = ToSyncNotificationPushNotification(notification); + var expectedNotification = ToNotificationPushNotification(notification, null); await sutProvider.Sut.PushNotificationAsync(notification); - await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, expectedSyncNotification, + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, + expectedNotification, + $"(template:payload_userId:{notification.UserId} && clientType:{clientType})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(ClientType.Browser)] + [BitAutoData(ClientType.Desktop)] + [BitAutoData(ClientType.Web)] + [BitAutoData(ClientType.Mobile)] + [NotificationCustomize(false)] + public async Task PushNotificationAsync_UserIdProvidedOrganizationIdProvidedClientTypeNotAll_SentToUser( + ClientType clientType, SutProvider sutProvider, + Notification notification) + { + notification.ClientType = clientType; + var expectedNotification = ToNotificationPushNotification(notification, null); + + await sutProvider.Sut.PushNotificationAsync(notification); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, + expectedNotification, $"(template:payload_userId:{notification.UserId} && clientType:{clientType})"); await sutProvider.GetDependency() .Received(0) @@ -94,16 +112,17 @@ public class NotificationHubPushNotificationServiceTests [Theory] [BitAutoData] [NotificationCustomize(false)] - public async void PushNotificationAsync_UserIdNullOrganizationIdProvidedClientTypeAll_SentToOrganization( + public async Task PushNotificationAsync_UserIdNullOrganizationIdProvidedClientTypeAll_SentToOrganization( SutProvider sutProvider, Notification notification) { notification.UserId = null; notification.ClientType = ClientType.All; - var expectedSyncNotification = ToSyncNotificationPushNotification(notification); + var expectedNotification = ToNotificationPushNotification(notification, null); await sutProvider.Sut.PushNotificationAsync(notification); - await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, expectedSyncNotification, + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, + expectedNotification, $"(template:payload && organizationId:{notification.OrganizationId})"); await sutProvider.GetDependency() .Received(0) @@ -116,18 +135,156 @@ public class NotificationHubPushNotificationServiceTests [BitAutoData(ClientType.Web)] [BitAutoData(ClientType.Mobile)] [NotificationCustomize(false)] - public async void PushNotificationAsync_UserIdNullOrganizationIdProvidedClientTypeNotAll_SentToOrganization( + public async Task PushNotificationAsync_UserIdNullOrganizationIdProvidedClientTypeNotAll_SentToOrganization( ClientType clientType, SutProvider sutProvider, Notification notification) { notification.UserId = null; notification.ClientType = clientType; - - var expectedSyncNotification = ToSyncNotificationPushNotification(notification); + var expectedNotification = ToNotificationPushNotification(notification, null); await sutProvider.Sut.PushNotificationAsync(notification); - await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, expectedSyncNotification, + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, + expectedNotification, + $"(template:payload && organizationId:{notification.OrganizationId} && clientType:{clientType})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + [NotificationCustomize] + public async Task PushNotificationStatusAsync_Global_NotSent( + SutProvider sutProvider, Notification notification, + NotificationStatus notificationStatus) + { + await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); + + await sutProvider.GetDependency() + .Received(0) + .AllClients + .Received(0) + .SendTemplateNotificationAsync(Arg.Any>(), Arg.Any()); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(false)] + [BitAutoData(true)] + [NotificationCustomize(false)] + public async Task PushNotificationStatusAsync_UserIdProvidedClientTypeAll_SentToUser( + bool organizationIdNull, SutProvider sutProvider, + Notification notification, NotificationStatus notificationStatus) + { + if (organizationIdNull) + { + notification.OrganizationId = null; + } + + notification.ClientType = ClientType.All; + var expectedNotification = ToNotificationPushNotification(notification, notificationStatus); + + await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotificationStatus, + expectedNotification, + $"(template:payload_userId:{notification.UserId})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(ClientType.Browser)] + [BitAutoData(ClientType.Desktop)] + [BitAutoData(ClientType.Web)] + [BitAutoData(ClientType.Mobile)] + [NotificationCustomize(false)] + public async Task PushNotificationStatusAsync_UserIdProvidedOrganizationIdNullClientTypeNotAll_SentToUser( + ClientType clientType, SutProvider sutProvider, + Notification notification, NotificationStatus notificationStatus) + { + notification.OrganizationId = null; + notification.ClientType = clientType; + var expectedNotification = ToNotificationPushNotification(notification, notificationStatus); + + await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotificationStatus, + expectedNotification, + $"(template:payload_userId:{notification.UserId} && clientType:{clientType})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(ClientType.Browser)] + [BitAutoData(ClientType.Desktop)] + [BitAutoData(ClientType.Web)] + [BitAutoData(ClientType.Mobile)] + [NotificationCustomize(false)] + public async Task PushNotificationStatusAsync_UserIdProvidedOrganizationIdProvidedClientTypeNotAll_SentToUser( + ClientType clientType, SutProvider sutProvider, + Notification notification, NotificationStatus notificationStatus) + { + notification.ClientType = clientType; + var expectedNotification = ToNotificationPushNotification(notification, notificationStatus); + + await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotificationStatus, + expectedNotification, + $"(template:payload_userId:{notification.UserId} && clientType:{clientType})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(false)] + public async Task PushNotificationStatusAsync_UserIdNullOrganizationIdProvidedClientTypeAll_SentToOrganization( + SutProvider sutProvider, Notification notification, + NotificationStatus notificationStatus) + { + notification.UserId = null; + notification.ClientType = ClientType.All; + var expectedNotification = ToNotificationPushNotification(notification, notificationStatus); + + await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotificationStatus, + expectedNotification, + $"(template:payload && organizationId:{notification.OrganizationId})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(ClientType.Browser)] + [BitAutoData(ClientType.Desktop)] + [BitAutoData(ClientType.Web)] + [BitAutoData(ClientType.Mobile)] + [NotificationCustomize(false)] + public async Task + PushNotificationStatusAsync_UserIdNullOrganizationIdProvidedClientTypeNotAll_SentToOrganization( + ClientType clientType, SutProvider sutProvider, + Notification notification, NotificationStatus notificationStatus) + { + notification.UserId = null; + notification.ClientType = clientType; + var expectedNotification = ToNotificationPushNotification(notification, notificationStatus); + + await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotificationStatus, + expectedNotification, $"(template:payload && organizationId:{notification.OrganizationId} && clientType:{clientType})"); await sutProvider.GetDependency() .Received(0) @@ -137,7 +294,7 @@ public class NotificationHubPushNotificationServiceTests [Theory] [BitAutoData([null])] [BitAutoData(ClientType.All)] - public async void SendPayloadToUserAsync_ClientTypeNullOrAll_SentToUser(ClientType? clientType, + public async Task SendPayloadToUserAsync_ClientTypeNullOrAll_SentToUser(ClientType? clientType, SutProvider sutProvider, Guid userId, PushType pushType, string payload, string identifier) { @@ -156,7 +313,7 @@ public class NotificationHubPushNotificationServiceTests [BitAutoData(ClientType.Desktop)] [BitAutoData(ClientType.Mobile)] [BitAutoData(ClientType.Web)] - public async void SendPayloadToUserAsync_ClientTypeExplicit_SentToUserAndClientType(ClientType clientType, + public async Task SendPayloadToUserAsync_ClientTypeExplicit_SentToUserAndClientType(ClientType clientType, SutProvider sutProvider, Guid userId, PushType pushType, string payload, string identifier) { @@ -173,7 +330,7 @@ public class NotificationHubPushNotificationServiceTests [Theory] [BitAutoData([null])] [BitAutoData(ClientType.All)] - public async void SendPayloadToOrganizationAsync_ClientTypeNullOrAll_SentToOrganization(ClientType? clientType, + public async Task SendPayloadToOrganizationAsync_ClientTypeNullOrAll_SentToOrganization(ClientType? clientType, SutProvider sutProvider, Guid organizationId, PushType pushType, string payload, string identifier) { @@ -192,7 +349,7 @@ public class NotificationHubPushNotificationServiceTests [BitAutoData(ClientType.Desktop)] [BitAutoData(ClientType.Mobile)] [BitAutoData(ClientType.Web)] - public async void SendPayloadToOrganizationAsync_ClientTypeExplicit_SentToOrganizationAndClientType( + public async Task SendPayloadToOrganizationAsync_ClientTypeExplicit_SentToOrganizationAndClientType( ClientType clientType, SutProvider sutProvider, Guid organizationId, PushType pushType, string payload, string identifier) { @@ -206,7 +363,8 @@ public class NotificationHubPushNotificationServiceTests .UpsertAsync(Arg.Any()); } - private static NotificationPushNotification ToSyncNotificationPushNotification(Notification notification) => + private static NotificationPushNotification ToNotificationPushNotification(Notification notification, + NotificationStatus? notificationStatus) => new() { Id = notification.Id, @@ -218,7 +376,9 @@ public class NotificationHubPushNotificationServiceTests Title = notification.Title, Body = notification.Body, CreationDate = notification.CreationDate, - RevisionDate = notification.RevisionDate + RevisionDate = notification.RevisionDate, + ReadDate = notificationStatus?.ReadDate, + DeletedDate = notificationStatus?.DeletedDate }; private static async Task AssertSendTemplateNotificationAsync( diff --git a/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs index a84a76152a..22161924ea 100644 --- a/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs @@ -24,7 +24,7 @@ public class AzureQueuePushNotificationServiceTests [BitAutoData] [NotificationCustomize] [CurrentContextCustomize] - public async void PushNotificationAsync_Notification_Sent( + public async Task PushNotificationAsync_Notification_Sent( SutProvider sutProvider, Notification notification, Guid deviceIdentifier, ICurrentContext currentContext) { @@ -36,7 +36,30 @@ public class AzureQueuePushNotificationServiceTests await sutProvider.GetDependency().Received(1) .SendMessageAsync(Arg.Is(message => - MatchMessage(PushType.SyncNotification, message, new SyncNotificationEquals(notification), + MatchMessage(PushType.SyncNotification, message, + new NotificationPushNotificationEquals(notification, null), + deviceIdentifier.ToString()))); + } + + [Theory] + [BitAutoData] + [NotificationCustomize] + [NotificationStatusCustomize] + [CurrentContextCustomize] + public async Task PushNotificationStatusAsync_Notification_Sent( + SutProvider sutProvider, Notification notification, Guid deviceIdentifier, + ICurrentContext currentContext, NotificationStatus notificationStatus) + { + currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); + sutProvider.GetDependency().HttpContext!.RequestServices + .GetService(Arg.Any()).Returns(currentContext); + + await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); + + await sutProvider.GetDependency().Received(1) + .SendMessageAsync(Arg.Is(message => + MatchMessage(PushType.SyncNotificationStatus, message, + new NotificationPushNotificationEquals(notification, notificationStatus), deviceIdentifier.ToString()))); } @@ -50,7 +73,8 @@ public class AzureQueuePushNotificationServiceTests pushNotificationData.ContextId == contextId; } - private class SyncNotificationEquals(Notification notification) : IEquatable + private class NotificationPushNotificationEquals(Notification notification, NotificationStatus? notificationStatus) + : IEquatable { public bool Equals(NotificationPushNotification? other) { @@ -66,7 +90,9 @@ public class AzureQueuePushNotificationServiceTests other.Title == notification.Title && other.Body == notification.Body && other.CreationDate == notification.CreationDate && - other.RevisionDate == notification.RevisionDate; + other.RevisionDate == notification.RevisionDate && + other.ReadDate == notificationStatus?.ReadDate && + other.DeletedDate == notificationStatus?.DeletedDate; } } } diff --git a/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs index edbd297708..08dfd0a5c0 100644 --- a/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs @@ -26,6 +26,22 @@ public class MultiServicePushNotificationServiceTests .PushNotificationAsync(notification); } + [Theory] + [BitAutoData] + [NotificationCustomize] + [NotificationStatusCustomize] + public async Task PushNotificationStatusAsync_Notification_Sent( + SutProvider sutProvider, Notification notification, + NotificationStatus notificationStatus) + { + await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); + + await sutProvider.GetDependency>() + .First() + .Received(1) + .PushNotificationStatusAsync(notification, notificationStatus); + } + [Theory] [BitAutoData([null, null])] [BitAutoData(ClientType.All, null)] From 6cb00ebc8e3550647c714ca94dd5a2de6f045974 Mon Sep 17 00:00:00 2001 From: rkac-bw <148072202+rkac-bw@users.noreply.github.com> Date: Thu, 13 Feb 2025 08:57:41 -0700 Subject: [PATCH 132/148] Add entity path to database test workflow (#5401) * Add entity path to database test workflow * Add entity path to pull request - path database test workflow --------- Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> --- .github/workflows/test-database.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index b7b06688b4..a668ddb37d 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -17,6 +17,7 @@ on: - "src/Infrastructure.Dapper/**" # Changes to SQL Server Dapper Repository Layer - "src/Infrastructure.EntityFramework/**" # Changes to Entity Framework Repository Layer - "test/Infrastructure.IntegrationTest/**" # Any changes to the tests + - "src/**/Entities/**/*.cs" # Database entity definitions pull_request: paths: - ".github/workflows/test-database.yml" # This file @@ -28,6 +29,7 @@ on: - "src/Infrastructure.Dapper/**" # Changes to SQL Server Dapper Repository Layer - "src/Infrastructure.EntityFramework/**" # Changes to Entity Framework Repository Layer - "test/Infrastructure.IntegrationTest/**" # Any changes to the tests + - "src/**/Entities/**/*.cs" # Database entity definitions jobs: check-test-secrets: From 465549b812eaa65dec0403f51ce93ca12759772d Mon Sep 17 00:00:00 2001 From: Graham Walker Date: Thu, 13 Feb 2025 12:43:34 -0600 Subject: [PATCH 133/148] PM-17954 changing import permissions around based on requirements (#5385) Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- src/Api/Tools/Controllers/ImportCiphersController.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Api/Tools/Controllers/ImportCiphersController.cs b/src/Api/Tools/Controllers/ImportCiphersController.cs index c268500f71..d6104de354 100644 --- a/src/Api/Tools/Controllers/ImportCiphersController.cs +++ b/src/Api/Tools/Controllers/ImportCiphersController.cs @@ -96,12 +96,6 @@ public class ImportCiphersController : Controller return true; } - //Users allowed to import if they CanCreate Collections - if (!(await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.Create)).Succeeded) - { - return false; - } - //Calling Repository instead of Service as we want to get all the collections, regardless of permission //Permissions check will be done later on AuthorizationService var orgCollectionIds = @@ -118,6 +112,12 @@ public class ImportCiphersController : Controller return false; }; + //Users allowed to import if they CanCreate Collections + if (!(await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.Create)).Succeeded) + { + return false; + } + return true; } } From ac6bc40d85c115fb012884756c94b05244bd8dbe Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Thu, 13 Feb 2025 15:51:36 -0500 Subject: [PATCH 134/148] feat(2FA): [PM-17129] Login with 2FA Recovery Code * feat(2FA): [PM-17129] Login with 2FA Recovery Code - Login with Recovery Code working. * feat(2FA): [PM-17129] Login with 2FA Recovery Code - Feature flagged implementation. * style(2FA): [PM-17129] Login with 2FA Recovery Code - Code cleanup. * test(2FA): [PM-17129] Login with 2FA Recovery Code - Tests. --- .../Auth/Controllers/TwoFactorController.cs | 28 +++++---- src/Core/Auth/Enums/TwoFactorProviderType.cs | 1 + src/Core/Constants.cs | 1 + src/Core/Services/IUserService.cs | 29 +++++++--- .../Services/Implementations/UserService.cs | 29 +++++++++- .../RequestValidators/BaseRequestValidator.cs | 56 ++++++++++-------- .../TwoFactorAuthenticationValidator.cs | 52 +++++++++++------ test/Core.Test/Services/UserServiceTests.cs | 40 +++++++++++++ .../BaseRequestValidatorTests.cs | 2 +- .../TwoFactorAuthenticationValidatorTests.cs | 58 ++++++++++++++++--- 10 files changed, 220 insertions(+), 76 deletions(-) diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index 2714b9aba3..c7d39f64b0 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -304,7 +304,7 @@ public class TwoFactorController : Controller if (user != null) { - // check if 2FA email is from passwordless + // Check if 2FA email is from Passwordless. if (!string.IsNullOrEmpty(requestModel.AuthRequestAccessCode)) { if (await _verifyAuthRequestCommand @@ -317,17 +317,14 @@ public class TwoFactorController : Controller } else if (!string.IsNullOrEmpty(requestModel.SsoEmail2FaSessionToken)) { - if (this.ValidateSsoEmail2FaToken(requestModel.SsoEmail2FaSessionToken, user)) + if (ValidateSsoEmail2FaToken(requestModel.SsoEmail2FaSessionToken, user)) { await _userService.SendTwoFactorEmailAsync(user); return; } - else - { - await this.ThrowDelayedBadRequestExceptionAsync( - "Cannot send two-factor email: a valid, non-expired SSO Email 2FA Session token is required to send 2FA emails.", - 2000); - } + + await ThrowDelayedBadRequestExceptionAsync( + "Cannot send two-factor email: a valid, non-expired SSO Email 2FA Session token is required to send 2FA emails."); } else if (await _userService.VerifySecretAsync(user, requestModel.Secret)) { @@ -336,8 +333,7 @@ public class TwoFactorController : Controller } } - await this.ThrowDelayedBadRequestExceptionAsync( - "Cannot send two-factor email.", 2000); + await ThrowDelayedBadRequestExceptionAsync("Cannot send two-factor email."); } [HttpPut("email")] @@ -374,7 +370,7 @@ public class TwoFactorController : Controller public async Task PutOrganizationDisable(string id, [FromBody] TwoFactorProviderRequestModel model) { - var user = await CheckAsync(model, false); + await CheckAsync(model, false); var orgIdGuid = new Guid(id); if (!await _currentContext.ManagePolicies(orgIdGuid)) @@ -401,6 +397,10 @@ public class TwoFactorController : Controller return response; } + /// + /// To be removed when the feature flag pm-17128-recovery-code-login is removed PM-18175. + /// + [Obsolete("Two Factor recovery is handled in the TwoFactorAuthenticationValidator.")] [HttpPost("recover")] [AllowAnonymous] public async Task PostRecover([FromBody] TwoFactorRecoveryRequestModel model) @@ -463,10 +463,8 @@ public class TwoFactorController : Controller await Task.Delay(2000); throw new BadRequestException(name, $"{name} is invalid."); } - else - { - await Task.Delay(500); - } + + await Task.Delay(500); } private bool ValidateSsoEmail2FaToken(string ssoEmail2FaSessionToken, User user) diff --git a/src/Core/Auth/Enums/TwoFactorProviderType.cs b/src/Core/Auth/Enums/TwoFactorProviderType.cs index a17b61c3cd..07a52dc429 100644 --- a/src/Core/Auth/Enums/TwoFactorProviderType.cs +++ b/src/Core/Auth/Enums/TwoFactorProviderType.cs @@ -10,4 +10,5 @@ public enum TwoFactorProviderType : byte Remember = 5, OrganizationDuo = 6, WebAuthn = 7, + RecoveryCode = 8, } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 6b3d0485b0..0b0d21f7bb 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -170,6 +170,7 @@ public static class FeatureFlagKeys public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync"; public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal"; public const string AndroidMutualTls = "mutual-tls"; + public const string RecoveryCodeLogin = "pm-17128-recovery-code-login"; public const string PM3503_MobileAnonAddySelfHostAlias = "anon-addy-self-host-alias"; public static List GetAllKeys() diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 0886d18897..d1c61e4418 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -22,7 +22,6 @@ public interface IUserService Task CreateUserAsync(User user, string masterPasswordHash); Task SendMasterPasswordHintAsync(string email); Task SendTwoFactorEmailAsync(User user); - Task VerifyTwoFactorEmailAsync(User user, string token); Task StartWebAuthnRegistrationAsync(User user); Task DeleteWebAuthnKeyAsync(User user, int id); Task CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse); @@ -41,8 +40,6 @@ public interface IUserService Task RefreshSecurityStampAsync(User user, string masterPasswordHash); Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type, bool setEnabled = true, bool logEvent = true); Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type); - Task RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode); - Task GenerateUserTokenAsync(User user, string tokenProvider, string purpose); Task DeleteAsync(User user); Task DeleteAsync(User user, string token); Task SendDeleteConfirmationAsync(string email); @@ -55,9 +52,7 @@ public interface IUserService Task CancelPremiumAsync(User user, bool? endOfPeriod = null); Task ReinstatePremiumAsync(User user); Task EnablePremiumAsync(Guid userId, DateTime? expirationDate); - Task EnablePremiumAsync(User user, DateTime? expirationDate); Task DisablePremiumAsync(Guid userId, DateTime? expirationDate); - Task DisablePremiumAsync(User user, DateTime? expirationDate); Task UpdatePremiumExpirationAsync(Guid userId, DateTime? expirationDate); Task GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null, int? version = null); @@ -91,9 +86,26 @@ public interface IUserService void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool setEnabled = true); + [Obsolete("To be removed when the feature flag pm-17128-recovery-code-login is removed PM-18175.")] + Task RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode); + /// - /// Returns true if the user is a legacy user. Legacy users use their master key as their encryption key. - /// We force these users to the web to migrate their encryption scheme. + /// This method is used by the TwoFactorAuthenticationValidator to recover two + /// factor for a user. This allows users to be logged in after a successful recovery + /// attempt. + /// + /// This method logs the event, sends an email to the user, and removes two factor + /// providers on the user account. This means that a user will have to accomplish + /// new device verification on their account on new logins, if it is enabled for their user. + /// + /// recovery code associated with the user logging in + /// The user to refresh the 2FA and Recovery Code on. + /// true if the recovery code is valid; false otherwise + Task RecoverTwoFactorAsync(User user, string recoveryCode); + + /// + /// Returns true if the user is a legacy user. Legacy users use their master key as their + /// encryption key. We force these users to the web to migrate their encryption scheme. /// Task IsLegacyUser(string userId); @@ -101,7 +113,8 @@ public interface IUserService /// Indicates if the user is managed by any organization. /// /// - /// A user is considered managed by an organization if their email domain matches one of the verified domains of that organization, and the user is a member of it. + /// A user is considered managed by an organization if their email domain matches one of the + /// verified domains of that organization, and the user is a member of it. /// The organization must be enabled and able to have verified domains. /// /// diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 11d4042def..e04290a686 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -315,7 +315,7 @@ public class UserService : UserManager, IUserService, IDisposable return; } - var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "DeleteAccount"); + var token = await GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "DeleteAccount"); await _mailService.SendVerifyDeleteEmailAsync(user.Email, user.Id, token); } @@ -868,6 +868,10 @@ public class UserService : UserManager, IUserService, IDisposable } } + /// + /// To be removed when the feature flag pm-17128-recovery-code-login is removed PM-18175. + /// + [Obsolete("Two Factor recovery is handled in the TwoFactorAuthenticationValidator.")] public async Task RecoverTwoFactorAsync(string email, string secret, string recoveryCode) { var user = await _userRepository.GetByEmailAsync(email); @@ -897,6 +901,25 @@ public class UserService : UserManager, IUserService, IDisposable return true; } + public async Task RecoverTwoFactorAsync(User user, string recoveryCode) + { + if (!CoreHelpers.FixedTimeEquals( + user.TwoFactorRecoveryCode, + recoveryCode.Replace(" ", string.Empty).Trim().ToLower())) + { + return false; + } + + user.TwoFactorProviders = null; + user.TwoFactorRecoveryCode = CoreHelpers.SecureRandomString(32, upper: false, special: false); + await SaveUserAsync(user); + await _mailService.SendRecoverTwoFactorEmail(user.Email, DateTime.UtcNow, _currentContext.IpAddress); + await _eventService.LogUserEventAsync(user.Id, EventType.User_Recovered2fa); + await CheckPoliciesOnTwoFactorRemovalAsync(user); + + return true; + } + public async Task> SignUpPremiumAsync(User user, string paymentToken, PaymentMethodType paymentMethodType, short additionalStorageGb, UserLicense license, TaxInfo taxInfo) @@ -1081,7 +1104,7 @@ public class UserService : UserManager, IUserService, IDisposable await EnablePremiumAsync(user, expirationDate); } - public async Task EnablePremiumAsync(User user, DateTime? expirationDate) + private async Task EnablePremiumAsync(User user, DateTime? expirationDate) { if (user != null && !user.Premium && user.Gateway.HasValue) { @@ -1098,7 +1121,7 @@ public class UserService : UserManager, IUserService, IDisposable await DisablePremiumAsync(user, expirationDate); } - public async Task DisablePremiumAsync(User user, DateTime? expirationDate) + private async Task DisablePremiumAsync(User user, DateTime? expirationDate) { if (user != null && user.Premium) { diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index 5e78212cf1..88691fa8f7 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -77,7 +77,7 @@ 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 + // 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; var valid = await ValidateContextAsync(context, validatorContext); var user = validatorContext.User; @@ -99,7 +99,7 @@ public abstract class BaseRequestValidator where T : class return; } - // 2. Does this user belong to an organization that requires SSO + // 2. Decide if this user belongs to an organization that requires SSO. validatorContext.SsoRequired = await RequireSsoLoginAsync(user, request.GrantType); if (validatorContext.SsoRequired) { @@ -111,17 +111,22 @@ public abstract class BaseRequestValidator where T : class 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 + // 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 twoFactorToken = request.Raw["TwoFactorToken"]; + var twoFactorProvider = request.Raw["TwoFactorProvider"]; var validTwoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) && !string.IsNullOrWhiteSpace(twoFactorProvider); - // response for 2FA required and not provided state + + // 3a. Response for 2FA required and not provided state. if (!validTwoFactorRequest || !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType)) { @@ -133,26 +138,27 @@ public abstract class BaseRequestValidator where T : class return; } - // Include Master Password Policy in 2FA response - resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user)); + // Include Master Password Policy in 2FA response. + resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user)); SetTwoFactorResult(context, resultDict); return; } - var twoFactorTokenValid = await _twoFactorAuthenticationValidator - .VerifyTwoFactor(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken); + var twoFactorTokenValid = + await _twoFactorAuthenticationValidator + .VerifyTwoFactorAsync(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken); - // response for 2FA required but request is not valid or remember token expired state + // 3b. Response for 2FA required but request is not valid or remember token expired state. if (!twoFactorTokenValid) { - // The remember me token has expired + // The remember me token has expired. if (twoFactorProviderType == TwoFactorProviderType.Remember) { var resultDict = await _twoFactorAuthenticationValidator .BuildTwoFactorResultAsync(user, twoFactorOrganization); // Include Master Password Policy in 2FA response - resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user)); + resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user)); SetTwoFactorResult(context, resultDict); } else @@ -163,17 +169,19 @@ public abstract class BaseRequestValidator where T : class return; } - // 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 + // 3c. When the 2FA authentication is successful, we can check if the user wants a + // rememberMe token. + var twoFactorRemember = request.Raw["TwoFactorRemember"] == "1"; + // Check if the user wants a rememberMe token. + if (twoFactorRemember + // if the 2FA auth was rememberMe do not send another token. + && twoFactorProviderType != TwoFactorProviderType.Remember) { returnRememberMeToken = true; } } - // 4. Check if the user is logging in from a new device + // 4. Check if the user is logging in from a new device. var deviceValid = await _deviceValidator.ValidateRequestDeviceAsync(request, validatorContext); if (!deviceValid) { @@ -182,7 +190,7 @@ public abstract class BaseRequestValidator where T : class return; } - // 5. Force legacy users to the web for migration + // 5. Force legacy users to the web for migration. if (UserService.IsLegacyUser(user) && request.ClientId != "web") { await FailAuthForLegacyUserAsync(user, context); @@ -224,7 +232,7 @@ public abstract class BaseRequestValidator where T : class customResponse.Add("Key", user.Key); } - customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user)); + customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user)); customResponse.Add("ForcePasswordReset", user.ForcePasswordReset); customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword)); customResponse.Add("Kdf", (byte)user.Kdf); @@ -403,7 +411,7 @@ public abstract class BaseRequestValidator where T : class return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling; } - private async Task GetMasterPasswordPolicy(User user) + private async Task GetMasterPasswordPolicyAsync(User user) { // Check current context/cache to see if user is in any organizations, avoids extra DB call if not var orgs = (await CurrentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)) diff --git a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs index e2c6406c89..856846cdd6 100644 --- a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity.TokenProviders; @@ -44,7 +45,7 @@ public interface ITwoFactorAuthenticationValidator /// Two Factor Provider to use to verify the token /// secret passed from the user and consumed by the two-factor provider's verify method /// boolean - Task VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType twoFactorProviderType, string token); + Task VerifyTwoFactorAsync(User user, Organization organization, TwoFactorProviderType twoFactorProviderType, string token); } public class TwoFactorAuthenticationValidator( @@ -139,7 +140,7 @@ public class TwoFactorAuthenticationValidator( return twoFactorResultDict; } - public async Task VerifyTwoFactor( + public async Task VerifyTwoFactorAsync( User user, Organization organization, TwoFactorProviderType type, @@ -154,24 +155,39 @@ public class TwoFactorAuthenticationValidator( return false; } - switch (type) + if (_featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin)) { - case TwoFactorProviderType.Authenticator: - case TwoFactorProviderType.Email: - case TwoFactorProviderType.Duo: - case TwoFactorProviderType.YubiKey: - case TwoFactorProviderType.WebAuthn: - case TwoFactorProviderType.Remember: - if (type != TwoFactorProviderType.Remember && - !await _userService.TwoFactorProviderIsEnabledAsync(type, user)) - { - return false; - } - return await _userManager.VerifyTwoFactorTokenAsync(user, - CoreHelpers.CustomProviderName(type), token); - default: - return false; + if (type is TwoFactorProviderType.RecoveryCode) + { + return await _userService.RecoverTwoFactorAsync(user, token); + } } + + // These cases we want to always return false, U2f is deprecated and OrganizationDuo + // uses a different flow than the other two factor providers, it follows the same + // structure of a UserTokenProvider but has it's logic ran outside the usual token + // provider flow. See IOrganizationDuoUniversalTokenProvider.cs + if (type is TwoFactorProviderType.U2f or TwoFactorProviderType.OrganizationDuo) + { + return false; + } + + // Now we are concerning the rest of the Two Factor Provider Types + + // The intent of this check is to make sure that the user is using a 2FA provider that + // is enabled and allowed by their premium status. The exception for Remember + // is because it is a "special" 2FA type that isn't ever explicitly + // enabled by a user, so we can't check the user's 2FA providers to see if they're + // enabled. We just have to check if the token is valid. + if (type != TwoFactorProviderType.Remember && + !await _userService.TwoFactorProviderIsEnabledAsync(type, user)) + { + return false; + } + + // Finally, verify the token based on the provider type. + return await _userManager.VerifyTwoFactorTokenAsync( + user, CoreHelpers.CustomProviderName(type), token); } private async Task>> GetEnabledTwoFactorProvidersAsync( diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 9539767f6f..88c214f471 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -730,6 +730,46 @@ public class UserServiceTests .RemoveAsync(Arg.Any()); } + [Theory, BitAutoData] + public async Task RecoverTwoFactorAsync_CorrectCode_ReturnsTrueAndProcessesPolicies( + User user, SutProvider sutProvider) + { + // Arrange + var recoveryCode = "1234"; + user.TwoFactorRecoveryCode = recoveryCode; + + // Act + var response = await sutProvider.Sut.RecoverTwoFactorAsync(user, recoveryCode); + + // Assert + Assert.True(response); + Assert.Null(user.TwoFactorProviders); + // Make sure a new code was generated for the user + Assert.NotEqual(recoveryCode, user.TwoFactorRecoveryCode); + await sutProvider.GetDependency() + .Received(1) + .SendRecoverTwoFactorEmail(Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .Received(1) + .LogUserEventAsync(user.Id, EventType.User_Recovered2fa); + } + + [Theory, BitAutoData] + public async Task RecoverTwoFactorAsync_IncorrectCode_ReturnsFalse( + User user, SutProvider sutProvider) + { + // Arrange + var recoveryCode = "1234"; + user.TwoFactorRecoveryCode = "4567"; + + // Act + var response = await sutProvider.Sut.RecoverTwoFactorAsync(user, recoveryCode); + + // Assert + Assert.False(response); + Assert.NotNull(user.TwoFactorProviders); + } + private static void SetupUserAndDevice(User user, bool shouldHavePassword) { diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index 916b52e1d0..589aac2842 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -105,7 +105,7 @@ public class BaseRequestValidatorTests // Assert await _eventService.Received(1) .LogUserEventAsync(context.CustomValidatorRequestContext.User.Id, - Core.Enums.EventType.User_FailedLogIn); + EventType.User_FailedLogIn); Assert.True(context.GrantResult.IsError); Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message); } diff --git a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs index dfb877b8d6..e59a66a9e7 100644 --- a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Models.Business.Tokenables; @@ -328,7 +329,7 @@ public class TwoFactorAuthenticationValidatorTests _userManager.TWO_FACTOR_PROVIDERS = ["email"]; // Act - var result = await _sut.VerifyTwoFactor( + var result = await _sut.VerifyTwoFactorAsync( user, null, TwoFactorProviderType.U2f, token); // Assert @@ -348,7 +349,7 @@ public class TwoFactorAuthenticationValidatorTests _userManager.TWO_FACTOR_PROVIDERS = ["email"]; // Act - var result = await _sut.VerifyTwoFactor( + var result = await _sut.VerifyTwoFactorAsync( user, null, TwoFactorProviderType.Email, token); // Assert @@ -368,7 +369,7 @@ public class TwoFactorAuthenticationValidatorTests _userManager.TWO_FACTOR_PROVIDERS = ["OrganizationDuo"]; // Act - var result = await _sut.VerifyTwoFactor( + var result = await _sut.VerifyTwoFactorAsync( user, null, TwoFactorProviderType.OrganizationDuo, token); // Assert @@ -394,7 +395,7 @@ public class TwoFactorAuthenticationValidatorTests _userManager.TWO_FACTOR_TOKEN_VERIFIED = true; // Act - var result = await _sut.VerifyTwoFactor(user, null, providerType, token); + var result = await _sut.VerifyTwoFactorAsync(user, null, providerType, token); // Assert Assert.True(result); @@ -419,7 +420,7 @@ public class TwoFactorAuthenticationValidatorTests _userManager.TWO_FACTOR_TOKEN_VERIFIED = false; // Act - var result = await _sut.VerifyTwoFactor(user, null, providerType, token); + var result = await _sut.VerifyTwoFactorAsync(user, null, providerType, token); // Assert Assert.False(result); @@ -445,13 +446,56 @@ public class TwoFactorAuthenticationValidatorTests organization.Enabled = true; // Act - var result = await _sut.VerifyTwoFactor( + var result = await _sut.VerifyTwoFactorAsync( user, organization, providerType, token); // Assert Assert.True(result); } + [Theory] + [BitAutoData(TwoFactorProviderType.RecoveryCode)] + public async void VerifyTwoFactorAsync_RecoveryCode_ValidToken_ReturnsTrue( + TwoFactorProviderType providerType, + User user, + Organization organization) + { + var token = "1234"; + user.TwoFactorRecoveryCode = token; + + _userService.RecoverTwoFactorAsync(Arg.Is(user), Arg.Is(token)).Returns(true); + _featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin).Returns(true); + + // Act + var result = await _sut.VerifyTwoFactorAsync( + user, organization, providerType, token); + + // Assert + Assert.True(result); + } + + [Theory] + [BitAutoData(TwoFactorProviderType.RecoveryCode)] + public async void VerifyTwoFactorAsync_RecoveryCode_InvalidToken_ReturnsFalse( + TwoFactorProviderType providerType, + User user, + Organization organization) + { + // Arrange + var token = "1234"; + user.TwoFactorRecoveryCode = token; + + _userService.RecoverTwoFactorAsync(Arg.Is(user), Arg.Is(token)).Returns(false); + _featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin).Returns(true); + + // Act + var result = await _sut.VerifyTwoFactorAsync( + user, organization, providerType, token); + + // Assert + Assert.False(result); + } + private static UserManagerTestWrapper SubstituteUserManager() { return new UserManagerTestWrapper( From 54d59b3b92711cd4263f85ef6a18c9c9652e39b3 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 14 Feb 2025 11:09:01 +1000 Subject: [PATCH 135/148] [PM-16812] Shortcut duplicate group patch requests (#5354) * Copy PatchGroupCommand to vNext and refactor * Detect duplicate add requests and return early * Update read repository method to use HA replica * Add new write repository method --- .../Scim/Controllers/v2/GroupsController.cs | 27 +- .../Interfaces/IPatchGroupCommandvNext.cs | 9 + .../src/Scim/Groups/PatchGroupCommandvNext.cs | 170 ++++++++ .../src/Scim/Utilities/ScimConstants.cs | 13 + .../ScimServiceCollectionExtensions.cs | 1 + .../v2/GroupsControllerPatchTests.cs | 237 +++++++++++ .../v2/GroupsControllerPatchTestsvNext.cs | 251 ++++++++++++ .../Controllers/v2/GroupsControllerTests.cs | 221 +--------- .../Factories/ScimApplicationFactory.cs | 40 +- .../Groups/PatchGroupCommandvNextTests.cs | 381 ++++++++++++++++++ .../Repositories/IGroupRepository.cs | 20 +- src/Core/Constants.cs | 1 + .../Repositories/GroupRepository.cs | 19 +- .../Repositories/GroupRepository.cs | 27 +- .../Stored Procedures/GroupUser_AddUsers.sql | 39 ++ .../AdminConsole/OrganizationTestHelpers.cs | 57 +++ .../Repositories/GroupRepositoryTests.cs | 129 ++++++ .../OrganizationDomainRepositoryTests.cs | 2 +- .../OrganizationRepositoryTests.cs | 2 +- .../OrganizationUserRepositoryTests.cs | 2 +- .../2025-02-13_00_GroupUser_AddUsers.sql | 39 ++ 21 files changed, 1437 insertions(+), 250 deletions(-) create mode 100644 bitwarden_license/src/Scim/Groups/Interfaces/IPatchGroupCommandvNext.cs create mode 100644 bitwarden_license/src/Scim/Groups/PatchGroupCommandvNext.cs create mode 100644 bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/GroupsControllerPatchTests.cs create mode 100644 bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/GroupsControllerPatchTestsvNext.cs create mode 100644 bitwarden_license/test/Scim.Test/Groups/PatchGroupCommandvNextTests.cs create mode 100644 src/Sql/dbo/Stored Procedures/GroupUser_AddUsers.sql create mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs create mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/Repositories/GroupRepositoryTests.cs create mode 100644 util/Migrator/DbScripts/2025-02-13_00_GroupUser_AddUsers.sql diff --git a/bitwarden_license/src/Scim/Controllers/v2/GroupsController.cs b/bitwarden_license/src/Scim/Controllers/v2/GroupsController.cs index 5df0b29216..8c21793a9d 100644 --- a/bitwarden_license/src/Scim/Controllers/v2/GroupsController.cs +++ b/bitwarden_license/src/Scim/Controllers/v2/GroupsController.cs @@ -1,8 +1,10 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; +using Bit.Core; +using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Scim.Groups.Interfaces; using Bit.Scim.Models; using Bit.Scim.Utilities; @@ -22,9 +24,10 @@ public class GroupsController : Controller private readonly IGetGroupsListQuery _getGroupsListQuery; private readonly IDeleteGroupCommand _deleteGroupCommand; private readonly IPatchGroupCommand _patchGroupCommand; + private readonly IPatchGroupCommandvNext _patchGroupCommandvNext; private readonly IPostGroupCommand _postGroupCommand; private readonly IPutGroupCommand _putGroupCommand; - private readonly ILogger _logger; + private readonly IFeatureService _featureService; public GroupsController( IGroupRepository groupRepository, @@ -32,18 +35,21 @@ public class GroupsController : Controller IGetGroupsListQuery getGroupsListQuery, IDeleteGroupCommand deleteGroupCommand, IPatchGroupCommand patchGroupCommand, + IPatchGroupCommandvNext patchGroupCommandvNext, IPostGroupCommand postGroupCommand, IPutGroupCommand putGroupCommand, - ILogger logger) + IFeatureService featureService + ) { _groupRepository = groupRepository; _organizationRepository = organizationRepository; _getGroupsListQuery = getGroupsListQuery; _deleteGroupCommand = deleteGroupCommand; _patchGroupCommand = patchGroupCommand; + _patchGroupCommandvNext = patchGroupCommandvNext; _postGroupCommand = postGroupCommand; _putGroupCommand = putGroupCommand; - _logger = logger; + _featureService = featureService; } [HttpGet("{id}")] @@ -97,8 +103,21 @@ public class GroupsController : Controller [HttpPatch("{id}")] public async Task Patch(Guid organizationId, Guid id, [FromBody] ScimPatchModel model) { + if (_featureService.IsEnabled(FeatureFlagKeys.ShortcutDuplicatePatchRequests)) + { + var group = await _groupRepository.GetByIdAsync(id); + if (group == null || group.OrganizationId != organizationId) + { + throw new NotFoundException("Group not found."); + } + + await _patchGroupCommandvNext.PatchGroupAsync(group, model); + return new NoContentResult(); + } + var organization = await _organizationRepository.GetByIdAsync(organizationId); await _patchGroupCommand.PatchGroupAsync(organization, id, model); + return new NoContentResult(); } diff --git a/bitwarden_license/src/Scim/Groups/Interfaces/IPatchGroupCommandvNext.cs b/bitwarden_license/src/Scim/Groups/Interfaces/IPatchGroupCommandvNext.cs new file mode 100644 index 0000000000..f51cc54079 --- /dev/null +++ b/bitwarden_license/src/Scim/Groups/Interfaces/IPatchGroupCommandvNext.cs @@ -0,0 +1,9 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Scim.Models; + +namespace Bit.Scim.Groups.Interfaces; + +public interface IPatchGroupCommandvNext +{ + Task PatchGroupAsync(Group group, ScimPatchModel model); +} diff --git a/bitwarden_license/src/Scim/Groups/PatchGroupCommandvNext.cs b/bitwarden_license/src/Scim/Groups/PatchGroupCommandvNext.cs new file mode 100644 index 0000000000..359df4bc94 --- /dev/null +++ b/bitwarden_license/src/Scim/Groups/PatchGroupCommandvNext.cs @@ -0,0 +1,170 @@ +using System.Text.Json; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.AdminConsole.Services; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Scim.Groups.Interfaces; +using Bit.Scim.Models; +using Bit.Scim.Utilities; + +namespace Bit.Scim.Groups; + +public class PatchGroupCommandvNext : IPatchGroupCommandvNext +{ + private readonly IGroupRepository _groupRepository; + private readonly IGroupService _groupService; + private readonly IUpdateGroupCommand _updateGroupCommand; + private readonly ILogger _logger; + private readonly IOrganizationRepository _organizationRepository; + + public PatchGroupCommandvNext( + IGroupRepository groupRepository, + IGroupService groupService, + IUpdateGroupCommand updateGroupCommand, + ILogger logger, + IOrganizationRepository organizationRepository) + { + _groupRepository = groupRepository; + _groupService = groupService; + _updateGroupCommand = updateGroupCommand; + _logger = logger; + _organizationRepository = organizationRepository; + } + + public async Task PatchGroupAsync(Group group, ScimPatchModel model) + { + foreach (var operation in model.Operations) + { + await HandleOperationAsync(group, operation); + } + } + + private async Task HandleOperationAsync(Group group, ScimPatchModel.OperationModel operation) + { + switch (operation.Op?.ToLowerInvariant()) + { + // Replace a list of members + case PatchOps.Replace when operation.Path?.ToLowerInvariant() == PatchPaths.Members: + { + var ids = GetOperationValueIds(operation.Value); + await _groupRepository.UpdateUsersAsync(group.Id, ids); + break; + } + + // Replace group name from path + case PatchOps.Replace when operation.Path?.ToLowerInvariant() == PatchPaths.DisplayName: + { + group.Name = operation.Value.GetString(); + var organization = await _organizationRepository.GetByIdAsync(group.OrganizationId); + if (organization == null) + { + throw new NotFoundException(); + } + await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM); + break; + } + + // Replace group name from value object + case PatchOps.Replace when + string.IsNullOrWhiteSpace(operation.Path) && + operation.Value.TryGetProperty("displayName", out var displayNameProperty): + { + group.Name = displayNameProperty.GetString(); + var organization = await _organizationRepository.GetByIdAsync(group.OrganizationId); + if (organization == null) + { + throw new NotFoundException(); + } + await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM); + break; + } + + // Add a single member + case PatchOps.Add when + !string.IsNullOrWhiteSpace(operation.Path) && + operation.Path.StartsWith("members[value eq ", StringComparison.OrdinalIgnoreCase) && + TryGetOperationPathId(operation.Path, out var addId): + { + await AddMembersAsync(group, [addId]); + break; + } + + // Add a list of members + case PatchOps.Add when + operation.Path?.ToLowerInvariant() == PatchPaths.Members: + { + await AddMembersAsync(group, GetOperationValueIds(operation.Value)); + break; + } + + // Remove a single member + case PatchOps.Remove when + !string.IsNullOrWhiteSpace(operation.Path) && + operation.Path.StartsWith("members[value eq ", StringComparison.OrdinalIgnoreCase) && + TryGetOperationPathId(operation.Path, out var removeId): + { + await _groupService.DeleteUserAsync(group, removeId, EventSystemUser.SCIM); + break; + } + + // Remove a list of members + case PatchOps.Remove when + operation.Path?.ToLowerInvariant() == PatchPaths.Members: + { + var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet(); + foreach (var v in GetOperationValueIds(operation.Value)) + { + orgUserIds.Remove(v); + } + await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds); + break; + } + + default: + { + _logger.LogWarning("Group patch operation not handled: {OperationOp}:{OperationPath}", operation.Op, operation.Path); + break; + } + } + } + + private async Task AddMembersAsync(Group group, HashSet usersToAdd) + { + // Azure Entra ID is known to send redundant "add" requests for each existing member every time any member + // is removed. To avoid excessive load on the database, we check against the high availability replica and + // return early if they already exist. + var groupMembers = await _groupRepository.GetManyUserIdsByIdAsync(group.Id, useReadOnlyReplica: true); + if (usersToAdd.IsSubsetOf(groupMembers)) + { + _logger.LogDebug("Ignoring duplicate SCIM request to add members {Members} to group {Group}", usersToAdd, group.Id); + return; + } + + await _groupRepository.AddGroupUsersByIdAsync(group.Id, usersToAdd); + } + + private static HashSet GetOperationValueIds(JsonElement objArray) + { + var ids = new HashSet(); + foreach (var obj in objArray.EnumerateArray()) + { + if (obj.TryGetProperty("value", out var valueProperty)) + { + if (valueProperty.TryGetGuid(out var guid)) + { + ids.Add(guid); + } + } + } + return ids; + } + + private static bool TryGetOperationPathId(string path, out Guid pathId) + { + // Parse Guid from string like: members[value eq "{GUID}"}] + return Guid.TryParse(path.Substring(18).Replace("\"]", string.Empty), out pathId); + } +} diff --git a/bitwarden_license/src/Scim/Utilities/ScimConstants.cs b/bitwarden_license/src/Scim/Utilities/ScimConstants.cs index 219be6534f..0836a72c7f 100644 --- a/bitwarden_license/src/Scim/Utilities/ScimConstants.cs +++ b/bitwarden_license/src/Scim/Utilities/ScimConstants.cs @@ -7,3 +7,16 @@ public static class ScimConstants public const string Scim2SchemaUser = "urn:ietf:params:scim:schemas:core:2.0:User"; public const string Scim2SchemaGroup = "urn:ietf:params:scim:schemas:core:2.0:Group"; } + +public static class PatchOps +{ + public const string Replace = "replace"; + public const string Add = "add"; + public const string Remove = "remove"; +} + +public static class PatchPaths +{ + public const string Members = "members"; + public const string DisplayName = "displayname"; +} diff --git a/bitwarden_license/src/Scim/Utilities/ScimServiceCollectionExtensions.cs b/bitwarden_license/src/Scim/Utilities/ScimServiceCollectionExtensions.cs index 75b60a71fc..b5d866524a 100644 --- a/bitwarden_license/src/Scim/Utilities/ScimServiceCollectionExtensions.cs +++ b/bitwarden_license/src/Scim/Utilities/ScimServiceCollectionExtensions.cs @@ -10,6 +10,7 @@ public static class ScimServiceCollectionExtensions public static void AddScimGroupCommands(this IServiceCollection services) { services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); } diff --git a/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/GroupsControllerPatchTests.cs b/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/GroupsControllerPatchTests.cs new file mode 100644 index 0000000000..eaa5b3dcd7 --- /dev/null +++ b/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/GroupsControllerPatchTests.cs @@ -0,0 +1,237 @@ +using System.Text.Json; +using Bit.Scim.IntegrationTest.Factories; +using Bit.Scim.Models; +using Bit.Scim.Utilities; +using Bit.Test.Common.Helpers; +using Xunit; + +namespace Bit.Scim.IntegrationTest.Controllers.v2; + +public class GroupsControllerPatchTests : IClassFixture, IAsyncLifetime +{ + private readonly ScimApplicationFactory _factory; + + public GroupsControllerPatchTests(ScimApplicationFactory factory) + { + _factory = factory; + } + + public Task InitializeAsync() + { + var databaseContext = _factory.GetDatabaseContext(); + _factory.ReinitializeDbForTests(databaseContext); + return Task.CompletedTask; + } + + Task IAsyncLifetime.DisposeAsync() => Task.CompletedTask; + + [Fact] + public async Task Patch_ReplaceDisplayName_Success() + { + var organizationId = ScimApplicationFactory.TestOrganizationId1; + var groupId = ScimApplicationFactory.TestGroupId1; + var newDisplayName = "Patch Display Name"; + var inputModel = new ScimPatchModel + { + Operations = new List() + { + new ScimPatchModel.OperationModel + { + Op = "replace", + Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement + } + }, + Schemas = new List() { ScimConstants.Scim2SchemaGroup } + }; + + var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); + + Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); + + var databaseContext = _factory.GetDatabaseContext(); + var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId); + Assert.Equal(newDisplayName, group.Name); + + Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount, databaseContext.GroupUsers.Count()); + Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1)); + Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4)); + } + + [Fact] + public async Task Patch_ReplaceMembers_Success() + { + var organizationId = ScimApplicationFactory.TestOrganizationId1; + var groupId = ScimApplicationFactory.TestGroupId1; + var inputModel = new ScimPatchModel + { + Operations = new List() + { + new ScimPatchModel.OperationModel + { + Op = "replace", + Path = "members", + Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}}]").RootElement + } + }, + Schemas = new List() { ScimConstants.Scim2SchemaGroup } + }; + + var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); + + Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); + + var databaseContext = _factory.GetDatabaseContext(); + Assert.Single(databaseContext.GroupUsers); + + Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count()); + var groupUser = databaseContext.GroupUsers.FirstOrDefault(); + Assert.Equal(ScimApplicationFactory.TestOrganizationUserId2, groupUser.OrganizationUserId); + } + + [Fact] + public async Task Patch_AddSingleMember_Success() + { + var organizationId = ScimApplicationFactory.TestOrganizationId1; + var groupId = ScimApplicationFactory.TestGroupId1; + var inputModel = new ScimPatchModel + { + Operations = new List() + { + new ScimPatchModel.OperationModel + { + Op = "add", + Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId2}\"]", + Value = JsonDocument.Parse("{}").RootElement + } + }, + Schemas = new List() { ScimConstants.Scim2SchemaGroup } + }; + + var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); + + Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); + + var databaseContext = _factory.GetDatabaseContext(); + Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount + 1, databaseContext.GroupUsers.Count()); + Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1)); + Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2)); + Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4)); + } + + [Fact] + public async Task Patch_AddListMembers_Success() + { + var organizationId = ScimApplicationFactory.TestOrganizationId1; + var groupId = ScimApplicationFactory.TestGroupId2; + var inputModel = new ScimPatchModel + { + Operations = new List() + { + new ScimPatchModel.OperationModel + { + Op = "add", + Path = "members", + Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}},{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId3}\"}}]").RootElement + } + }, + Schemas = new List() { ScimConstants.Scim2SchemaGroup } + }; + + var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); + + Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); + + var databaseContext = _factory.GetDatabaseContext(); + Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2)); + Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId3)); + } + + [Fact] + public async Task Patch_RemoveSingleMember_ReplaceDisplayName_Success() + { + var organizationId = ScimApplicationFactory.TestOrganizationId1; + var groupId = ScimApplicationFactory.TestGroupId1; + var newDisplayName = "Patch Display Name"; + var inputModel = new ScimPatchModel + { + Operations = new List() + { + new ScimPatchModel.OperationModel + { + Op = "remove", + Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId1}\"]", + Value = JsonDocument.Parse("{}").RootElement + }, + new ScimPatchModel.OperationModel + { + Op = "replace", + Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement + } + }, + Schemas = new List() { ScimConstants.Scim2SchemaGroup } + }; + + var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); + + Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); + + var databaseContext = _factory.GetDatabaseContext(); + Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count()); + Assert.Equal(ScimApplicationFactory.InitialGroupCount, databaseContext.Groups.Count()); + + var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId); + Assert.Equal(newDisplayName, group.Name); + } + + [Fact] + public async Task Patch_RemoveListMembers_Success() + { + var organizationId = ScimApplicationFactory.TestOrganizationId1; + var groupId = ScimApplicationFactory.TestGroupId1; + var inputModel = new ScimPatchModel + { + Operations = new List() + { + new ScimPatchModel.OperationModel + { + Op = "remove", + Path = "members", + Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId1}\"}}, {{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId4}\"}}]").RootElement + } + }, + Schemas = new List() { ScimConstants.Scim2SchemaGroup } + }; + + var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); + + Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); + + var databaseContext = _factory.GetDatabaseContext(); + Assert.Empty(databaseContext.GroupUsers); + } + + [Fact] + public async Task Patch_NotFound() + { + var organizationId = ScimApplicationFactory.TestOrganizationId1; + var groupId = Guid.NewGuid(); + var inputModel = new Models.ScimPatchModel + { + Operations = new List(), + Schemas = new List() { ScimConstants.Scim2SchemaGroup } + }; + var expectedResponse = new ScimErrorResponseModel + { + Status = StatusCodes.Status404NotFound, + Detail = "Group not found.", + Schemas = new List { ScimConstants.Scim2SchemaError } + }; + + var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); + + Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode); + + var responseModel = JsonSerializer.Deserialize(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + AssertHelper.AssertPropertyEqual(expectedResponse, responseModel); + } +} diff --git a/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/GroupsControllerPatchTestsvNext.cs b/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/GroupsControllerPatchTestsvNext.cs new file mode 100644 index 0000000000..f66184a8a2 --- /dev/null +++ b/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/GroupsControllerPatchTestsvNext.cs @@ -0,0 +1,251 @@ +using System.Text.Json; +using Bit.Core; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Services; +using Bit.Scim.Groups.Interfaces; +using Bit.Scim.IntegrationTest.Factories; +using Bit.Scim.Models; +using Bit.Scim.Utilities; +using Bit.Test.Common.Helpers; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Scim.IntegrationTest.Controllers.v2; + +public class GroupsControllerPatchTestsvNext : IClassFixture, IAsyncLifetime +{ + private readonly ScimApplicationFactory _factory; + + public GroupsControllerPatchTestsvNext(ScimApplicationFactory factory) + { + _factory = factory; + + // Enable the feature flag for new PatchGroupsCommand and stub out the old command to be safe + _factory.SubstituteService((IFeatureService featureService) + => featureService.IsEnabled(FeatureFlagKeys.ShortcutDuplicatePatchRequests).Returns(true)); + _factory.SubstituteService((IPatchGroupCommand patchGroupCommand) + => patchGroupCommand.PatchGroupAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception("This test suite should be testing the vNext command, but the existing command was called."))); + } + + public Task InitializeAsync() + { + var databaseContext = _factory.GetDatabaseContext(); + _factory.ReinitializeDbForTests(databaseContext); + + return Task.CompletedTask; + } + + Task IAsyncLifetime.DisposeAsync() => Task.CompletedTask; + + [Fact] + public async Task Patch_ReplaceDisplayName_Success() + { + var organizationId = ScimApplicationFactory.TestOrganizationId1; + var groupId = ScimApplicationFactory.TestGroupId1; + var newDisplayName = "Patch Display Name"; + var inputModel = new ScimPatchModel + { + Operations = new List() + { + new ScimPatchModel.OperationModel + { + Op = "replace", + Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement + } + }, + Schemas = new List() { ScimConstants.Scim2SchemaGroup } + }; + + var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); + + Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); + + var databaseContext = _factory.GetDatabaseContext(); + var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId); + Assert.Equal(newDisplayName, group.Name); + + Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount, databaseContext.GroupUsers.Count()); + Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1)); + Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4)); + } + + [Fact] + public async Task Patch_ReplaceMembers_Success() + { + var organizationId = ScimApplicationFactory.TestOrganizationId1; + var groupId = ScimApplicationFactory.TestGroupId1; + var inputModel = new ScimPatchModel + { + Operations = new List() + { + new ScimPatchModel.OperationModel + { + Op = "replace", + Path = "members", + Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}}]").RootElement + } + }, + Schemas = new List() { ScimConstants.Scim2SchemaGroup } + }; + + var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); + + Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); + + var databaseContext = _factory.GetDatabaseContext(); + Assert.Single(databaseContext.GroupUsers); + + Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count()); + var groupUser = databaseContext.GroupUsers.FirstOrDefault(); + Assert.Equal(ScimApplicationFactory.TestOrganizationUserId2, groupUser.OrganizationUserId); + } + + [Fact] + public async Task Patch_AddSingleMember_Success() + { + var organizationId = ScimApplicationFactory.TestOrganizationId1; + var groupId = ScimApplicationFactory.TestGroupId1; + var inputModel = new ScimPatchModel + { + Operations = new List() + { + new ScimPatchModel.OperationModel + { + Op = "add", + Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId2}\"]", + Value = JsonDocument.Parse("{}").RootElement + } + }, + Schemas = new List() { ScimConstants.Scim2SchemaGroup } + }; + + var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); + + Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); + + var databaseContext = _factory.GetDatabaseContext(); + Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount + 1, databaseContext.GroupUsers.Count()); + Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1)); + Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2)); + Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4)); + } + + [Fact] + public async Task Patch_AddListMembers_Success() + { + var organizationId = ScimApplicationFactory.TestOrganizationId1; + var groupId = ScimApplicationFactory.TestGroupId2; + var inputModel = new ScimPatchModel + { + Operations = new List() + { + new ScimPatchModel.OperationModel + { + Op = "add", + Path = "members", + Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}},{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId3}\"}}]").RootElement + } + }, + Schemas = new List() { ScimConstants.Scim2SchemaGroup } + }; + + var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); + + Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); + + var databaseContext = _factory.GetDatabaseContext(); + Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2)); + Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId3)); + } + + [Fact] + public async Task Patch_RemoveSingleMember_ReplaceDisplayName_Success() + { + var organizationId = ScimApplicationFactory.TestOrganizationId1; + var groupId = ScimApplicationFactory.TestGroupId1; + var newDisplayName = "Patch Display Name"; + var inputModel = new ScimPatchModel + { + Operations = new List() + { + new ScimPatchModel.OperationModel + { + Op = "remove", + Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId1}\"]", + Value = JsonDocument.Parse("{}").RootElement + }, + new ScimPatchModel.OperationModel + { + Op = "replace", + Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement + } + }, + Schemas = new List() { ScimConstants.Scim2SchemaGroup } + }; + + var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); + + Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); + + var databaseContext = _factory.GetDatabaseContext(); + Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count()); + Assert.Equal(ScimApplicationFactory.InitialGroupCount, databaseContext.Groups.Count()); + + var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId); + Assert.Equal(newDisplayName, group.Name); + } + + [Fact] + public async Task Patch_RemoveListMembers_Success() + { + var organizationId = ScimApplicationFactory.TestOrganizationId1; + var groupId = ScimApplicationFactory.TestGroupId1; + var inputModel = new ScimPatchModel + { + Operations = new List() + { + new ScimPatchModel.OperationModel + { + Op = "remove", + Path = "members", + Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId1}\"}}, {{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId4}\"}}]").RootElement + } + }, + Schemas = new List() { ScimConstants.Scim2SchemaGroup } + }; + + var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); + + Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); + + var databaseContext = _factory.GetDatabaseContext(); + Assert.Empty(databaseContext.GroupUsers); + } + + [Fact] + public async Task Patch_NotFound() + { + var organizationId = ScimApplicationFactory.TestOrganizationId1; + var groupId = Guid.NewGuid(); + var inputModel = new Models.ScimPatchModel + { + Operations = new List(), + Schemas = new List() { ScimConstants.Scim2SchemaGroup } + }; + var expectedResponse = new ScimErrorResponseModel + { + Status = StatusCodes.Status404NotFound, + Detail = "Group not found.", + Schemas = new List { ScimConstants.Scim2SchemaError } + }; + + var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); + + Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode); + + var responseModel = JsonSerializer.Deserialize(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + AssertHelper.AssertPropertyEqual(expectedResponse, responseModel); + } +} diff --git a/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/GroupsControllerTests.cs b/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/GroupsControllerTests.cs index f8150fc1c5..5f562a30c5 100644 --- a/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/GroupsControllerTests.cs +++ b/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/GroupsControllerTests.cs @@ -9,9 +9,6 @@ namespace Bit.Scim.IntegrationTest.Controllers.v2; public class GroupsControllerTests : IClassFixture, IAsyncLifetime { - private const int _initialGroupCount = 3; - private const int _initialGroupUsersCount = 2; - private readonly ScimApplicationFactory _factory; public GroupsControllerTests(ScimApplicationFactory factory) @@ -237,10 +234,10 @@ public class GroupsControllerTests : IClassFixture, IAsy AssertHelper.AssertPropertyEqual(expectedResponse, responseModel, "Id"); var databaseContext = _factory.GetDatabaseContext(); - Assert.Equal(_initialGroupCount + 1, databaseContext.Groups.Count()); + Assert.Equal(ScimApplicationFactory.InitialGroupCount + 1, databaseContext.Groups.Count()); Assert.True(databaseContext.Groups.Any(g => g.Name == displayName && g.ExternalId == externalId)); - Assert.Equal(_initialGroupUsersCount + 1, databaseContext.GroupUsers.Count()); + Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount + 1, databaseContext.GroupUsers.Count()); Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == responseModel.Id && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1)); } @@ -281,7 +278,7 @@ public class GroupsControllerTests : IClassFixture, IAsy Assert.Equal(StatusCodes.Status409Conflict, context.Response.StatusCode); var databaseContext = _factory.GetDatabaseContext(); - Assert.Equal(_initialGroupCount, databaseContext.Groups.Count()); + Assert.Equal(ScimApplicationFactory.InitialGroupCount, databaseContext.Groups.Count()); Assert.False(databaseContext.Groups.Any(g => g.Name == "New Group")); } @@ -354,216 +351,6 @@ public class GroupsControllerTests : IClassFixture, IAsy AssertHelper.AssertPropertyEqual(expectedResponse, responseModel); } - [Fact] - public async Task Patch_ReplaceDisplayName_Success() - { - var organizationId = ScimApplicationFactory.TestOrganizationId1; - var groupId = ScimApplicationFactory.TestGroupId1; - var newDisplayName = "Patch Display Name"; - var inputModel = new ScimPatchModel - { - Operations = new List() - { - new ScimPatchModel.OperationModel - { - Op = "replace", - Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement - } - }, - Schemas = new List() { ScimConstants.Scim2SchemaGroup } - }; - - var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); - - Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); - - var databaseContext = _factory.GetDatabaseContext(); - var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId); - Assert.Equal(newDisplayName, group.Name); - - Assert.Equal(_initialGroupUsersCount, databaseContext.GroupUsers.Count()); - Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1)); - Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4)); - } - - [Fact] - public async Task Patch_ReplaceMembers_Success() - { - var organizationId = ScimApplicationFactory.TestOrganizationId1; - var groupId = ScimApplicationFactory.TestGroupId1; - var inputModel = new ScimPatchModel - { - Operations = new List() - { - new ScimPatchModel.OperationModel - { - Op = "replace", - Path = "members", - Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}}]").RootElement - } - }, - Schemas = new List() { ScimConstants.Scim2SchemaGroup } - }; - - var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); - - Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); - - var databaseContext = _factory.GetDatabaseContext(); - Assert.Single(databaseContext.GroupUsers); - - Assert.Equal(_initialGroupUsersCount - 1, databaseContext.GroupUsers.Count()); - var groupUser = databaseContext.GroupUsers.FirstOrDefault(); - Assert.Equal(ScimApplicationFactory.TestOrganizationUserId2, groupUser.OrganizationUserId); - } - - [Fact] - public async Task Patch_AddSingleMember_Success() - { - var organizationId = ScimApplicationFactory.TestOrganizationId1; - var groupId = ScimApplicationFactory.TestGroupId1; - var inputModel = new ScimPatchModel - { - Operations = new List() - { - new ScimPatchModel.OperationModel - { - Op = "add", - Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId2}\"]", - Value = JsonDocument.Parse("{}").RootElement - } - }, - Schemas = new List() { ScimConstants.Scim2SchemaGroup } - }; - - var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); - - Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); - - var databaseContext = _factory.GetDatabaseContext(); - Assert.Equal(_initialGroupUsersCount + 1, databaseContext.GroupUsers.Count()); - Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1)); - Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2)); - Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4)); - } - - [Fact] - public async Task Patch_AddListMembers_Success() - { - var organizationId = ScimApplicationFactory.TestOrganizationId1; - var groupId = ScimApplicationFactory.TestGroupId2; - var inputModel = new ScimPatchModel - { - Operations = new List() - { - new ScimPatchModel.OperationModel - { - Op = "add", - Path = "members", - Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}},{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId3}\"}}]").RootElement - } - }, - Schemas = new List() { ScimConstants.Scim2SchemaGroup } - }; - - var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); - - Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); - - var databaseContext = _factory.GetDatabaseContext(); - Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2)); - Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId3)); - } - - [Fact] - public async Task Patch_RemoveSingleMember_ReplaceDisplayName_Success() - { - var organizationId = ScimApplicationFactory.TestOrganizationId1; - var groupId = ScimApplicationFactory.TestGroupId1; - var newDisplayName = "Patch Display Name"; - var inputModel = new ScimPatchModel - { - Operations = new List() - { - new ScimPatchModel.OperationModel - { - Op = "remove", - Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId1}\"]", - Value = JsonDocument.Parse("{}").RootElement - }, - new ScimPatchModel.OperationModel - { - Op = "replace", - Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement - } - }, - Schemas = new List() { ScimConstants.Scim2SchemaGroup } - }; - - var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); - - Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); - - var databaseContext = _factory.GetDatabaseContext(); - Assert.Equal(_initialGroupUsersCount - 1, databaseContext.GroupUsers.Count()); - Assert.Equal(_initialGroupCount, databaseContext.Groups.Count()); - - var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId); - Assert.Equal(newDisplayName, group.Name); - } - - [Fact] - public async Task Patch_RemoveListMembers_Success() - { - var organizationId = ScimApplicationFactory.TestOrganizationId1; - var groupId = ScimApplicationFactory.TestGroupId1; - var inputModel = new ScimPatchModel - { - Operations = new List() - { - new ScimPatchModel.OperationModel - { - Op = "remove", - Path = "members", - Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId1}\"}}, {{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId4}\"}}]").RootElement - } - }, - Schemas = new List() { ScimConstants.Scim2SchemaGroup } - }; - - var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); - - Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); - - var databaseContext = _factory.GetDatabaseContext(); - Assert.Empty(databaseContext.GroupUsers); - } - - [Fact] - public async Task Patch_NotFound() - { - var organizationId = ScimApplicationFactory.TestOrganizationId1; - var groupId = Guid.NewGuid(); - var inputModel = new Models.ScimPatchModel - { - Operations = new List(), - Schemas = new List() { ScimConstants.Scim2SchemaGroup } - }; - var expectedResponse = new ScimErrorResponseModel - { - Status = StatusCodes.Status404NotFound, - Detail = "Group not found.", - Schemas = new List { ScimConstants.Scim2SchemaError } - }; - - var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel); - - Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode); - - var responseModel = JsonSerializer.Deserialize(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - AssertHelper.AssertPropertyEqual(expectedResponse, responseModel); - } - [Fact] public async Task Delete_Success() { @@ -575,7 +362,7 @@ public class GroupsControllerTests : IClassFixture, IAsy Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); var databaseContext = _factory.GetDatabaseContext(); - Assert.Equal(_initialGroupCount - 1, databaseContext.Groups.Count()); + Assert.Equal(ScimApplicationFactory.InitialGroupCount - 1, databaseContext.Groups.Count()); Assert.True(databaseContext.Groups.FirstOrDefault(g => g.Id == groupId) == null); } diff --git a/bitwarden_license/test/Scim.IntegrationTest/Factories/ScimApplicationFactory.cs b/bitwarden_license/test/Scim.IntegrationTest/Factories/ScimApplicationFactory.cs index b9c6191f75..1a5cdde41c 100644 --- a/bitwarden_license/test/Scim.IntegrationTest/Factories/ScimApplicationFactory.cs +++ b/bitwarden_license/test/Scim.IntegrationTest/Factories/ScimApplicationFactory.cs @@ -9,8 +9,6 @@ using Bit.Infrastructure.EntityFramework.Repositories; using Bit.IntegrationTestCommon.Factories; using Bit.Scim.Models; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; @@ -18,7 +16,8 @@ namespace Bit.Scim.IntegrationTest.Factories; public class ScimApplicationFactory : WebApplicationFactoryBase { - public readonly new TestServer Server; + public const int InitialGroupCount = 3; + public const int InitialGroupUsersCount = 2; public static readonly Guid TestUserId1 = Guid.Parse("2e8173db-8e8d-4de1-ac38-91b15c6d8dcb"); public static readonly Guid TestUserId2 = Guid.Parse("b57846fc-0e94-4c93-9de5-9d0389eeadfb"); @@ -33,32 +32,29 @@ public class ScimApplicationFactory : WebApplicationFactoryBase public static readonly Guid TestOrganizationUserId3 = Guid.Parse("be2f9045-e2b6-4173-ad44-4c69c3ea8140"); public static readonly Guid TestOrganizationUserId4 = Guid.Parse("1f5689b7-e96e-4840-b0b1-eb3d5b5fd514"); - public ScimApplicationFactory() + protected override void ConfigureWebHost(IWebHostBuilder builder) { - WebApplicationFactory webApplicationFactory = WithWebHostBuilder(builder => + base.ConfigureWebHost(builder); + + builder.ConfigureServices(services => { - builder.ConfigureServices(services => + services + .AddAuthentication("Test") + .AddScheme("Test", options => { }); + + // Override to bypass SCIM authorization + services.AddAuthorization(config => { - services - .AddAuthentication("Test") - .AddScheme("Test", options => { }); - - // Override to bypass SCIM authorization - services.AddAuthorization(config => + config.AddPolicy("Scim", policy => { - config.AddPolicy("Scim", policy => - { - policy.RequireAssertion(a => true); - }); + policy.RequireAssertion(a => true); }); - - var mailService = services.First(sd => sd.ServiceType == typeof(IMailService)); - services.Remove(mailService); - services.AddSingleton(); }); - }); - Server = webApplicationFactory.Server; + var mailService = services.First(sd => sd.ServiceType == typeof(IMailService)); + services.Remove(mailService); + services.AddSingleton(); + }); } public async Task GroupsGetAsync(Guid organizationId, Guid id) diff --git a/bitwarden_license/test/Scim.Test/Groups/PatchGroupCommandvNextTests.cs b/bitwarden_license/test/Scim.Test/Groups/PatchGroupCommandvNextTests.cs new file mode 100644 index 0000000000..b9877f0b71 --- /dev/null +++ b/bitwarden_license/test/Scim.Test/Groups/PatchGroupCommandvNextTests.cs @@ -0,0 +1,381 @@ +using System.Text.Json; +using AutoFixture; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.AdminConsole.Services; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Scim.Groups; +using Bit.Scim.Models; +using Bit.Scim.Utilities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Scim.Test.Groups; + +[SutProviderCustomize] +public class PatchGroupCommandvNextTests +{ + [Theory] + [BitAutoData] + public async Task PatchGroup_ReplaceListMembers_Success(SutProvider sutProvider, + Organization organization, Group group, IEnumerable userIds) + { + group.OrganizationId = organization.Id; + + var scimPatchModel = new ScimPatchModel + { + Operations = new List + { + new() + { + Op = "replace", + Path = "members", + Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement + } + }, + Schemas = new List { ScimConstants.Scim2SchemaUser } + }; + + await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel); + + await sutProvider.GetDependency().Received(1).UpdateUsersAsync( + group.Id, + Arg.Is>(arg => + arg.Count() == userIds.Count() && + arg.ToHashSet().SetEquals(userIds))); + } + + [Theory] + [BitAutoData] + public async Task PatchGroup_ReplaceDisplayNameFromPath_Success( + SutProvider sutProvider, Organization organization, Group group, string displayName) + { + group.OrganizationId = organization.Id; + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + var scimPatchModel = new ScimPatchModel + { + Operations = new List + { + new() + { + Op = "replace", + Path = "displayname", + Value = JsonDocument.Parse($"\"{displayName}\"").RootElement + } + }, + Schemas = new List { ScimConstants.Scim2SchemaUser } + }; + + await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel); + + await sutProvider.GetDependency().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM); + Assert.Equal(displayName, group.Name); + } + + [Theory] + [BitAutoData] + public async Task PatchGroup_ReplaceDisplayNameFromValueObject_Success(SutProvider sutProvider, Organization organization, Group group, string displayName) + { + group.OrganizationId = organization.Id; + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + var scimPatchModel = new ScimPatchModel + { + Operations = new List + { + new() + { + Op = "replace", + Value = JsonDocument.Parse($"{{\"displayName\":\"{displayName}\"}}").RootElement + } + }, + Schemas = new List { ScimConstants.Scim2SchemaUser } + }; + + await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel); + + await sutProvider.GetDependency().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM); + Assert.Equal(displayName, group.Name); + } + + [Theory] + [BitAutoData] + public async Task PatchGroup_AddSingleMember_Success(SutProvider sutProvider, Organization organization, Group group, ICollection existingMembers, Guid userId) + { + group.OrganizationId = organization.Id; + + sutProvider.GetDependency() + .GetManyUserIdsByIdAsync(group.Id, true) + .Returns(existingMembers); + + var scimPatchModel = new ScimPatchModel + { + Operations = new List + { + new() + { + Op = "add", + Path = $"members[value eq \"{userId}\"]", + } + }, + Schemas = new List { ScimConstants.Scim2SchemaUser } + }; + + await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel); + + await sutProvider.GetDependency().Received(1).AddGroupUsersByIdAsync( + group.Id, + Arg.Is>(arg => arg.Single() == userId)); + } + + [Theory] + [BitAutoData] + public async Task PatchGroup_AddSingleMember_ReturnsEarlyIfAlreadyInGroup( + SutProvider sutProvider, + Organization organization, + Group group, + ICollection existingMembers) + { + // User being added is already in group + var userId = existingMembers.First(); + group.OrganizationId = organization.Id; + + sutProvider.GetDependency() + .GetManyUserIdsByIdAsync(group.Id, true) + .Returns(existingMembers); + + var scimPatchModel = new ScimPatchModel + { + Operations = new List + { + new() + { + Op = "add", + Path = $"members[value eq \"{userId}\"]", + } + }, + Schemas = new List { ScimConstants.Scim2SchemaUser } + }; + + await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .AddGroupUsersByIdAsync(default, default); + } + + [Theory] + [BitAutoData] + public async Task PatchGroup_AddListMembers_Success(SutProvider sutProvider, Organization organization, Group group, ICollection existingMembers, ICollection userIds) + { + group.OrganizationId = organization.Id; + + sutProvider.GetDependency() + .GetManyUserIdsByIdAsync(group.Id, true) + .Returns(existingMembers); + + var scimPatchModel = new ScimPatchModel + { + Operations = new List + { + new() + { + Op = "add", + Path = $"members", + Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement + } + }, + Schemas = new List { ScimConstants.Scim2SchemaUser } + }; + + await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel); + + await sutProvider.GetDependency().Received(1).AddGroupUsersByIdAsync( + group.Id, + Arg.Is>(arg => + arg.Count() == userIds.Count && + arg.ToHashSet().SetEquals(userIds))); + } + + [Theory] + [BitAutoData] + public async Task PatchGroup_AddListMembers_IgnoresDuplicatesInRequest( + SutProvider sutProvider, Organization organization, Group group, + ICollection existingMembers) + { + // Create 3 userIds + var fixture = new Fixture { RepeatCount = 3 }; + var userIds = fixture.CreateMany().ToList(); + + // Copy the list and add a duplicate + var userIdsWithDuplicate = userIds.Append(userIds.First()).ToList(); + Assert.Equal(4, userIdsWithDuplicate.Count); + + group.OrganizationId = organization.Id; + + sutProvider.GetDependency() + .GetManyUserIdsByIdAsync(group.Id, true) + .Returns(existingMembers); + + var scimPatchModel = new ScimPatchModel + { + Operations = new List + { + new() + { + Op = "add", + Path = $"members", + Value = JsonDocument.Parse(JsonSerializer + .Serialize(userIdsWithDuplicate + .Select(uid => new { value = uid }) + .ToArray())).RootElement + } + }, + Schemas = new List { ScimConstants.Scim2SchemaUser } + }; + + await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel); + + await sutProvider.GetDependency().Received(1).AddGroupUsersByIdAsync( + group.Id, + Arg.Is>(arg => + arg.Count() == 3 && + arg.ToHashSet().SetEquals(userIds))); + } + + [Theory] + [BitAutoData] + public async Task PatchGroup_AddListMembers_SuccessIfOnlySomeUsersAreInGroup( + SutProvider sutProvider, + Organization organization, Group group, + ICollection existingMembers, + ICollection userIds) + { + // A user is already in the group, but some still need to be added + userIds.Add(existingMembers.First()); + + group.OrganizationId = organization.Id; + + sutProvider.GetDependency() + .GetManyUserIdsByIdAsync(group.Id, true) + .Returns(existingMembers); + + var scimPatchModel = new ScimPatchModel + { + Operations = new List + { + new() + { + Op = "add", + Path = $"members", + Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement + } + }, + Schemas = new List { ScimConstants.Scim2SchemaUser } + }; + + await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel); + + await sutProvider.GetDependency() + .Received(1) + .AddGroupUsersByIdAsync( + group.Id, + Arg.Is>(arg => + arg.Count() == userIds.Count && + arg.ToHashSet().SetEquals(userIds))); + } + + [Theory] + [BitAutoData] + public async Task PatchGroup_RemoveSingleMember_Success(SutProvider sutProvider, Organization organization, Group group, Guid userId) + { + group.OrganizationId = organization.Id; + + var scimPatchModel = new Models.ScimPatchModel + { + Operations = new List + { + new ScimPatchModel.OperationModel + { + Op = "remove", + Path = $"members[value eq \"{userId}\"]", + } + }, + Schemas = new List { ScimConstants.Scim2SchemaUser } + }; + + await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel); + + await sutProvider.GetDependency().Received(1).DeleteUserAsync(group, userId, EventSystemUser.SCIM); + } + + [Theory] + [BitAutoData] + public async Task PatchGroup_RemoveListMembers_Success(SutProvider sutProvider, + Organization organization, Group group, ICollection existingMembers) + { + List usersToRemove = [existingMembers.First(), existingMembers.Skip(1).First()]; + group.OrganizationId = organization.Id; + + sutProvider.GetDependency() + .GetManyUserIdsByIdAsync(group.Id) + .Returns(existingMembers); + + var scimPatchModel = new Models.ScimPatchModel + { + Operations = new List + { + new() + { + Op = "remove", + Path = $"members", + Value = JsonDocument.Parse(JsonSerializer.Serialize(usersToRemove.Select(uid => new { value = uid }).ToArray())).RootElement + } + }, + Schemas = new List { ScimConstants.Scim2SchemaUser } + }; + + await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel); + + var expectedRemainingUsers = existingMembers.Skip(2).ToList(); + await sutProvider.GetDependency() + .Received(1) + .UpdateUsersAsync( + group.Id, + Arg.Is>(arg => + arg.Count() == expectedRemainingUsers.Count && + arg.ToHashSet().SetEquals(expectedRemainingUsers))); + } + + [Theory] + [BitAutoData] + public async Task PatchGroup_NoAction_Success( + SutProvider sutProvider, Organization organization, Group group) + { + group.OrganizationId = organization.Id; + + var scimPatchModel = new Models.ScimPatchModel + { + Operations = new List(), + Schemas = new List { ScimConstants.Scim2SchemaUser } + }; + + await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyUserIdsByIdAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().UpdateGroupAsync(default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default); + } +} diff --git a/src/Core/AdminConsole/Repositories/IGroupRepository.cs b/src/Core/AdminConsole/Repositories/IGroupRepository.cs index 6519b19833..b70331a3f5 100644 --- a/src/Core/AdminConsole/Repositories/IGroupRepository.cs +++ b/src/Core/AdminConsole/Repositories/IGroupRepository.cs @@ -14,11 +14,29 @@ public interface IGroupRepository : IRepository Guid organizationId); Task> GetManyByManyIds(IEnumerable groupIds); Task> GetManyIdsByUserIdAsync(Guid organizationUserId); - Task> GetManyUserIdsByIdAsync(Guid id); + /// + /// Query all OrganizationUserIds who are a member of the specified group. + /// + /// The group id. + /// + /// Whether to use the high-availability database replica. This is for paths with high traffic where immediate data + /// consistency is not required. You generally do not want this. + /// + /// + Task> GetManyUserIdsByIdAsync(Guid id, bool useReadOnlyReplica = false); Task> GetManyGroupUsersByOrganizationIdAsync(Guid organizationId); Task CreateAsync(Group obj, IEnumerable collections); Task ReplaceAsync(Group obj, IEnumerable collections); Task DeleteUserAsync(Guid groupId, Guid organizationUserId); + /// + /// Update a group's members. Replaces all members currently in the group. + /// Ignores members that do not belong to the same organization as the group. + /// Task UpdateUsersAsync(Guid groupId, IEnumerable organizationUserIds); + /// + /// Add members to a group. Gracefully ignores members that are already in the group, + /// duplicate organizationUserIds, and organizationUsers who are not part of the organization. + /// + Task AddGroupUsersByIdAsync(Guid groupId, IEnumerable organizationUserIds); Task DeleteManyAsync(IEnumerable groupIds); } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 0b0d21f7bb..21360703a2 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -108,6 +108,7 @@ public static class FeatureFlagKeys public const string IntegrationPage = "pm-14505-admin-console-integration-page"; public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications"; public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission"; + public const string ShortcutDuplicatePatchRequests = "pm-16812-shortcut-duplicate-patch-requests"; public const string PushSyncOrgKeysOnRevokeRestore = "pm-17168-push-sync-org-keys-on-revoke-restore"; /* Tools Team */ diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/GroupRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/GroupRepository.cs index d8245ce719..2b4db3940c 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/GroupRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/GroupRepository.cs @@ -109,9 +109,13 @@ public class GroupRepository : Repository, IGroupRepository } } - public async Task> GetManyUserIdsByIdAsync(Guid id) + public async Task> GetManyUserIdsByIdAsync(Guid id, bool useReadOnlyReplica = false) { - using (var connection = new SqlConnection(ConnectionString)) + var connectionString = useReadOnlyReplica + ? ReadOnlyConnectionString + : ConnectionString; + + using (var connection = new SqlConnection(connectionString)) { var results = await connection.QueryAsync( $"[{Schema}].[GroupUser_ReadOrganizationUserIdsByGroupId]", @@ -186,6 +190,17 @@ public class GroupRepository : Repository, IGroupRepository } } + public async Task AddGroupUsersByIdAsync(Guid groupId, IEnumerable organizationUserIds) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + "[dbo].[GroupUser_AddUsers]", + new { GroupId = groupId, OrganizationUserIds = organizationUserIds.ToGuidIdArrayTVP() }, + commandType: CommandType.StoredProcedure); + } + } + public async Task DeleteManyAsync(IEnumerable groupIds) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/GroupRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/GroupRepository.cs index 0e91bd42ef..305a715d4c 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/GroupRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/GroupRepository.cs @@ -163,8 +163,10 @@ public class GroupRepository : Repository> GetManyUserIdsByIdAsync(Guid id) + public async Task> GetManyUserIdsByIdAsync(Guid id, bool useReadOnlyReplica = false) { + // EF is only used for self-hosted so read-only replica parameter is ignored + using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); @@ -255,6 +257,29 @@ public class GroupRepository : Repository organizationUserIds) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var orgId = (await dbContext.Groups.FindAsync(groupId)).OrganizationId; + var insert = from ou in dbContext.OrganizationUsers + where organizationUserIds.Contains(ou.Id) && + ou.OrganizationId == orgId && + !dbContext.GroupUsers.Any(gu => gu.GroupId == groupId && ou.Id == gu.OrganizationUserId) + select new GroupUser + { + GroupId = groupId, + OrganizationUserId = ou.Id, + }; + await dbContext.AddRangeAsync(insert); + + await dbContext.SaveChangesAsync(); + await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(orgId); + await dbContext.SaveChangesAsync(); + } + } + public async Task DeleteManyAsync(IEnumerable groupIds) { using (var scope = ServiceScopeFactory.CreateScope()) diff --git a/src/Sql/dbo/Stored Procedures/GroupUser_AddUsers.sql b/src/Sql/dbo/Stored Procedures/GroupUser_AddUsers.sql new file mode 100644 index 0000000000..362cdce785 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/GroupUser_AddUsers.sql @@ -0,0 +1,39 @@ +CREATE PROCEDURE [dbo].[GroupUser_AddUsers] + @GroupId UNIQUEIDENTIFIER, + @OrganizationUserIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + DECLARE @OrgId UNIQUEIDENTIFIER = ( + SELECT TOP 1 + [OrganizationId] + FROM + [dbo].[Group] + WHERE + [Id] = @GroupId + ) + + -- Insert + INSERT INTO + [dbo].[GroupUser] (GroupId, OrganizationUserId) + SELECT DISTINCT + @GroupId, + [Source].[Id] + FROM + @OrganizationUserIds AS [Source] + INNER JOIN + [dbo].[OrganizationUser] OU ON [Source].[Id] = OU.[Id] AND OU.[OrganizationId] = @OrgId + WHERE + NOT EXISTS ( + SELECT + 1 + FROM + [dbo].[GroupUser] + WHERE + [GroupId] = @GroupId + AND [OrganizationUserId] = [Source].[Id] + ) + + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId +END diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs b/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs new file mode 100644 index 0000000000..e631280bb3 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs @@ -0,0 +1,57 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole; + +/// +/// A set of extension methods used to arrange simple test data. +/// This should only be used for basic, repetitive data arrangement, not for anything complex or for +/// the repository method under test. +/// +public static class OrganizationTestHelpers +{ + public static Task CreateTestUserAsync(this IUserRepository userRepository, string identifier = "test") + { + var id = Guid.NewGuid(); + return userRepository.CreateAsync(new User + { + Id = id, + Name = $"{identifier}-{id}", + Email = $"{id}@example.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + } + + public static Task CreateTestOrganizationAsync(this IOrganizationRepository organizationRepository, + string identifier = "test") + => organizationRepository.CreateAsync(new Organization + { + Name = $"{identifier}-{Guid.NewGuid()}", + BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL + Plan = "Test", // TODO: EF does not enforce this being NOT NULl + }); + + public static Task CreateTestOrganizationUserAsync( + this IOrganizationUserRepository organizationUserRepository, + Organization organization, + User user) + => organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner + }); + + public static Task CreateTestGroupAsync( + this IGroupRepository groupRepository, + Organization organization, + string identifier = "test") + => groupRepository.CreateAsync( + new Group { OrganizationId = organization.Id, Name = $"{identifier} {Guid.NewGuid()}" } + ); +} diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/GroupRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/GroupRepositoryTests.cs new file mode 100644 index 0000000000..e2c2cbfa02 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/GroupRepositoryTests.cs @@ -0,0 +1,129 @@ +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories; + +public class GroupRepositoryTests +{ + [DatabaseTheory, DatabaseData] + public async Task AddGroupUsersByIdAsync_CreatesGroupUsers( + IGroupRepository groupRepository, + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository) + { + // Arrange + var user1 = await userRepository.CreateTestUserAsync("user1"); + var user2 = await userRepository.CreateTestUserAsync("user2"); + var user3 = await userRepository.CreateTestUserAsync("user3"); + + var org = await organizationRepository.CreateTestOrganizationAsync(); + var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user1); + var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user2); + var orgUser3 = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user3); + var orgUserIds = new List([orgUser1.Id, orgUser2.Id, orgUser3.Id]); + var group = await groupRepository.CreateTestGroupAsync(org); + + // Act + await groupRepository.AddGroupUsersByIdAsync(group.Id, orgUserIds); + + // Assert + var actual = await groupRepository.GetManyUserIdsByIdAsync(group.Id); + Assert.Equal(orgUserIds!.Order(), actual.Order()); + } + + [DatabaseTheory, DatabaseData] + public async Task AddGroupUsersByIdAsync_IgnoresExistingGroupUsers( + IGroupRepository groupRepository, + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository) + { + // Arrange + var user1 = await userRepository.CreateTestUserAsync("user1"); + var user2 = await userRepository.CreateTestUserAsync("user2"); + var user3 = await userRepository.CreateTestUserAsync("user3"); + + var org = await organizationRepository.CreateTestOrganizationAsync(); + var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user1); + var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user2); + var orgUser3 = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user3); + var orgUserIds = new List([orgUser1.Id, orgUser2.Id, orgUser3.Id]); + var group = await groupRepository.CreateTestGroupAsync(org); + + // Add user 2 to the group already, make sure this is executed correctly before proceeding + await groupRepository.UpdateUsersAsync(group.Id, [orgUser2.Id]); + var existingUsers = await groupRepository.GetManyUserIdsByIdAsync(group.Id); + Assert.Equal([orgUser2.Id], existingUsers); + + // Act + await groupRepository.AddGroupUsersByIdAsync(group.Id, orgUserIds); + + // Assert - group should contain all users + var actual = await groupRepository.GetManyUserIdsByIdAsync(group.Id); + Assert.Equal(orgUserIds!.Order(), actual.Order()); + } + + [DatabaseTheory, DatabaseData] + public async Task AddGroupUsersByIdAsync_IgnoresUsersNotInOrganization( + IGroupRepository groupRepository, + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository) + { + // Arrange + var user1 = await userRepository.CreateTestUserAsync("user1"); + var user2 = await userRepository.CreateTestUserAsync("user2"); + var user3 = await userRepository.CreateTestUserAsync("user3"); + + var org = await organizationRepository.CreateTestOrganizationAsync(); + var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user1); + var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user2); + + // User3 belongs to a different org + var otherOrg = await organizationRepository.CreateTestOrganizationAsync(); + var orgUser3 = await organizationUserRepository.CreateTestOrganizationUserAsync(otherOrg, user3); + + var orgUserIds = new List([orgUser1.Id, orgUser2.Id, orgUser3.Id]); + var group = await groupRepository.CreateTestGroupAsync(org); + + // Act + await groupRepository.AddGroupUsersByIdAsync(group.Id, orgUserIds); + + // Assert + var actual = await groupRepository.GetManyUserIdsByIdAsync(group.Id); + Assert.Equal(2, actual.Count); + Assert.Contains(orgUser1.Id, actual); + Assert.Contains(orgUser2.Id, actual); + Assert.DoesNotContain(orgUser3.Id, actual); + } + + [DatabaseTheory, DatabaseData] + public async Task AddGroupUsersByIdAsync_IgnoresDuplicateUsers( + IGroupRepository groupRepository, + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository) + { + // Arrange + var user1 = await userRepository.CreateTestUserAsync("user1"); + var user2 = await userRepository.CreateTestUserAsync("user2"); + + var org = await organizationRepository.CreateTestOrganizationAsync(); + var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user1); + var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user2); + + var orgUserIds = new List([orgUser1.Id, orgUser2.Id, orgUser2.Id]); // duplicate orgUser2 + var group = await groupRepository.CreateTestGroupAsync(org); + + // Act + await groupRepository.AddGroupUsersByIdAsync(group.Id, orgUserIds); + + // Assert + var actual = await groupRepository.GetManyUserIdsByIdAsync(group.Id); + Assert.Equal(2, actual.Count); + Assert.Contains(orgUser1.Id, actual); + Assert.Contains(orgUser2.Id, actual); + } +} diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs index 7f0ed582bf..a1c5f9bd07 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs @@ -3,7 +3,7 @@ using Bit.Core.Entities; using Bit.Core.Repositories; using Xunit; -namespace Bit.Infrastructure.IntegrationTest.Repositories; +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories; public class OrganizationDomainRepositoryTests { diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs index f6dc4a989d..f7c61ad957 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs @@ -4,7 +4,7 @@ using Bit.Core.Enums; using Bit.Core.Repositories; using Xunit; -namespace Bit.Infrastructure.IntegrationTest.Repositories; +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories; public class OrganizationRepositoryTests { diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs index aee4beb8ce..e82be49173 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs @@ -4,7 +4,7 @@ using Bit.Core.Enums; using Bit.Core.Repositories; using Xunit; -namespace Bit.Infrastructure.IntegrationTest.Repositories; +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories; public class OrganizationUserRepositoryTests { diff --git a/util/Migrator/DbScripts/2025-02-13_00_GroupUser_AddUsers.sql b/util/Migrator/DbScripts/2025-02-13_00_GroupUser_AddUsers.sql new file mode 100644 index 0000000000..46ea72003e --- /dev/null +++ b/util/Migrator/DbScripts/2025-02-13_00_GroupUser_AddUsers.sql @@ -0,0 +1,39 @@ +CREATE OR ALTER PROCEDURE [dbo].[GroupUser_AddUsers] + @GroupId UNIQUEIDENTIFIER, + @OrganizationUserIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + DECLARE @OrgId UNIQUEIDENTIFIER = ( + SELECT TOP 1 + [OrganizationId] + FROM + [dbo].[Group] + WHERE + [Id] = @GroupId + ) + + -- Insert + INSERT INTO + [dbo].[GroupUser] (GroupId, OrganizationUserId) + SELECT DISTINCT + @GroupId, + [Source].[Id] + FROM + @OrganizationUserIds AS [Source] + INNER JOIN + [dbo].[OrganizationUser] OU ON [Source].[Id] = OU.[Id] AND OU.[OrganizationId] = @OrgId + WHERE + NOT EXISTS ( + SELECT + 1 + FROM + [dbo].[GroupUser] + WHERE + [GroupId] = @GroupId + AND [OrganizationUserId] = [Source].[Id] + ) + + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId +END From f4341b2f3bed714e651439f3191190e1aea07997 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 14 Feb 2025 21:05:49 +1000 Subject: [PATCH 136/148] [PM-14439] Add PolicyRequirementQuery for enforcement logic (#5336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add PolicyRequirementQuery, helpers and models in preparation for migrating domain code Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> --- .../Organizations/Policies/PolicyDetails.cs | 39 ++ .../Policies/IPolicyRequirementQuery.cs | 18 + .../Implementations/PolicyRequirementQuery.cs | 28 ++ .../PolicyRequirements/IPolicyRequirement.cs | 24 ++ .../PolicyRequirementHelpers.cs | 41 ++ .../PolicyServiceCollectionExtensions.cs | 38 ++ .../Repositories/IPolicyRepository.cs | 20 + src/Core/Constants.cs | 1 + .../Repositories/PolicyRepository.cs | 14 + .../Repositories/PolicyRepository.cs | 41 ++ .../PolicyDetails_ReadByUserId.sql | 43 ++ .../Policies/PolicyRequirementQueryTests.cs | 60 +++ .../GetPolicyDetailsByUserIdTests.cs | 385 ++++++++++++++++++ ...25-02-14_00_PolicyDetails_ReadByUserId.sql | 43 ++ 14 files changed, 795 insertions(+) create mode 100644 src/Core/AdminConsole/Models/Data/Organizations/Policies/PolicyDetails.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs create mode 100644 src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByUserId.sql create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs create mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs create mode 100644 util/Migrator/DbScripts/2025-02-14_00_PolicyDetails_ReadByUserId.sql diff --git a/src/Core/AdminConsole/Models/Data/Organizations/Policies/PolicyDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/Policies/PolicyDetails.cs new file mode 100644 index 0000000000..5b5db85f65 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/Organizations/Policies/PolicyDetails.cs @@ -0,0 +1,39 @@ +#nullable enable + +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Utilities; + +namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies; + +/// +/// Represents an OrganizationUser and a Policy which *may* be enforced against them. +/// You may assume that the Policy is enabled and that the organization's plan supports policies. +/// This is consumed by to create requirements for specific policy types. +/// +public class PolicyDetails +{ + public Guid OrganizationUserId { get; set; } + public Guid OrganizationId { get; set; } + public PolicyType PolicyType { get; set; } + public string? PolicyData { get; set; } + public OrganizationUserType OrganizationUserType { get; set; } + public OrganizationUserStatusType OrganizationUserStatus { get; set; } + /// + /// Custom permissions for the organization user, if any. Use + /// to deserialize. + /// + public string? OrganizationUserPermissionsData { get; set; } + /// + /// True if the user is also a ProviderUser for the organization, false otherwise. + /// + public bool IsProvider { get; set; } + + public T GetDataModel() where T : IPolicyDataModel, new() + => CoreHelpers.LoadClassFromJsonData(PolicyData); + + public Permissions GetOrganizationUserCustomPermissions() + => CoreHelpers.LoadClassFromJsonData(OrganizationUserPermissionsData); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs new file mode 100644 index 0000000000..5736078f22 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs @@ -0,0 +1,18 @@ +#nullable enable + +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies; + +public interface IPolicyRequirementQuery +{ + /// + /// Get a policy requirement for a specific user. + /// The policy requirement represents how one or more policy types should be enforced against the user. + /// It will always return a value even if there are no policies that should be enforced. + /// This should be used for all policy checks. + /// + /// The user that you need to enforce the policy against. + /// The IPolicyRequirement that corresponds to the policy you want to enforce. + Task GetAsync(Guid userId) where T : IPolicyRequirement; +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs new file mode 100644 index 0000000000..585d2348ef --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -0,0 +1,28 @@ +#nullable enable + +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.AdminConsole.Repositories; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; + +public class PolicyRequirementQuery( + IPolicyRepository policyRepository, + IEnumerable> factories) + : IPolicyRequirementQuery +{ + public async Task GetAsync(Guid userId) where T : IPolicyRequirement + { + var factory = factories.OfType>().SingleOrDefault(); + if (factory is null) + { + throw new NotImplementedException("No Policy Requirement found for " + typeof(T)); + } + + return factory(await GetPolicyDetails(userId)); + } + + private Task> GetPolicyDetails(Guid userId) => + policyRepository.GetPolicyDetailsByUserId(userId); +} + diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs new file mode 100644 index 0000000000..3f331b1130 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs @@ -0,0 +1,24 @@ +#nullable enable + +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +/// +/// Represents the business requirements of how one or more enterprise policies will be enforced against a user. +/// The implementation of this interface will depend on how the policies are enforced in the relevant domain. +/// +public interface IPolicyRequirement; + +/// +/// A factory function that takes a sequence of and transforms them into a single +/// for consumption by the relevant domain. This will receive *all* policy types +/// that may be enforced against a user; when implementing this delegate, you must filter out irrelevant policy types +/// as well as policies that should not be enforced against a user (e.g. due to the user's role or status). +/// +/// +/// See for extension methods to handle common requirements when implementing +/// this delegate. +/// +public delegate T RequirementFactory(IEnumerable policyDetails) + where T : IPolicyRequirement; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs new file mode 100644 index 0000000000..fc4cd91a3d --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs @@ -0,0 +1,41 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.Enums; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +public static class PolicyRequirementHelpers +{ + /// + /// Filters the PolicyDetails by PolicyType. This is generally required to only get the PolicyDetails that your + /// IPolicyRequirement relates to. + /// + public static IEnumerable GetPolicyType( + this IEnumerable policyDetails, + PolicyType type) + => policyDetails.Where(x => x.PolicyType == type); + + /// + /// Filters the PolicyDetails to remove the specified user roles. This can be used to exempt + /// owners and admins from policy enforcement. + /// + public static IEnumerable ExemptRoles( + this IEnumerable policyDetails, + IEnumerable roles) + => policyDetails.Where(x => !roles.Contains(x.OrganizationUserType)); + + /// + /// Filters the PolicyDetails to remove organization users who are also provider users for the organization. + /// This can be used to exempt provider users from policy enforcement. + /// + public static IEnumerable ExemptProviders(this IEnumerable policyDetails) + => policyDetails.Where(x => !x.IsProvider); + + /// + /// Filters the PolicyDetails to remove the specified organization user statuses. For example, this can be used + /// to exempt users in the invited and revoked statuses from policy enforcement. + /// + public static IEnumerable ExemptStatus( + this IEnumerable policyDetails, IEnumerable status) + => policyDetails.Where(x => !status.Contains(x.OrganizationUserStatus)); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index 4e88976c10..f7b35f2f06 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services.Implementations; @@ -12,7 +13,14 @@ public static class PolicyServiceCollectionExtensions { services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddPolicyValidators(); + services.AddPolicyRequirements(); + } + + private static void AddPolicyValidators(this IServiceCollection services) + { services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -20,4 +28,34 @@ public static class PolicyServiceCollectionExtensions services.AddScoped(); services.AddScoped(); } + + private static void AddPolicyRequirements(this IServiceCollection services) + { + // Register policy requirement factories here + } + + /// + /// Used to register simple policy requirements where its factory method implements CreateRequirement. + /// This MUST be used rather than calling AddScoped directly, because it will ensure the factory method has + /// the correct type to be injected and then identified by at runtime. + /// + /// The specific PolicyRequirement being registered. + private static void AddPolicyRequirement(this IServiceCollection serviceCollection, RequirementFactory factory) + where T : class, IPolicyRequirement + => serviceCollection.AddPolicyRequirement(_ => factory); + + /// + /// Used to register policy requirements where you need to access additional dependencies (usually to return a + /// curried factory method). + /// This MUST be used rather than calling AddScoped directly, because it will ensure the factory method has + /// the correct type to be injected and then identified by at runtime. + /// + /// + /// A callback that takes IServiceProvider and returns a RequirementFactory for + /// your policy requirement. + /// + private static void AddPolicyRequirement(this IServiceCollection serviceCollection, + Func> factory) + where T : class, IPolicyRequirement + => serviceCollection.AddScoped>(factory); } diff --git a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs index ad0654dd3c..4c0c03536d 100644 --- a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs +++ b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs @@ -1,5 +1,7 @@ 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.Repositories; #nullable enable @@ -8,7 +10,25 @@ namespace Bit.Core.AdminConsole.Repositories; public interface IPolicyRepository : IRepository { + /// + /// Gets all policies of a given type for an organization. + /// + /// + /// WARNING: do not use this to enforce policies against a user! It returns raw data and does not take into account + /// various business rules. Use instead. + /// Task GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type); Task> GetManyByOrganizationIdAsync(Guid organizationId); Task> GetManyByUserIdAsync(Guid userId); + /// + /// Gets all PolicyDetails for a user for all policy types. + /// + /// + /// Each PolicyDetail represents an OrganizationUser and a Policy which *may* be enforced + /// against them. It only returns PolicyDetails for policies that are enabled and where the organization's plan + /// supports policies. It also excludes "revoked invited" users who are not subject to policy enforcement. + /// This is consumed by to create requirements for specific policy types. + /// You probably do not want to call it directly. + /// + Task> GetPolicyDetailsByUserId(Guid userId); } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 21360703a2..91637e3893 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -110,6 +110,7 @@ public static class FeatureFlagKeys public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission"; public const string ShortcutDuplicatePatchRequests = "pm-16812-shortcut-duplicate-patch-requests"; public const string PushSyncOrgKeysOnRevokeRestore = "pm-17168-push-sync-org-keys-on-revoke-restore"; + public const string PolicyRequirements = "pm-14439-policy-requirements"; /* Tools Team */ public const string ItemShare = "item-share"; diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs index 196f3e3733..071ff3153a 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs @@ -1,6 +1,7 @@ using System.Data; 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.Settings; using Bit.Infrastructure.Dapper.Repositories; @@ -59,4 +60,17 @@ public class PolicyRepository : Repository, IPolicyRepository return results.ToList(); } } + + public async Task> GetPolicyDetailsByUserId(Guid userId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[PolicyDetails_ReadByUserId]", + new { UserId = userId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs index 3eb4ac934b..0564681341 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs @@ -1,6 +1,8 @@ using AutoMapper; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Enums; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries; using Bit.Infrastructure.EntityFramework.Repositories; @@ -50,4 +52,43 @@ public class PolicyRepository : Repository>(results); } } + + public async Task> GetPolicyDetailsByUserId(Guid userId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + var providerOrganizations = from pu in dbContext.ProviderUsers + where pu.UserId == userId + join po in dbContext.ProviderOrganizations + on pu.ProviderId equals po.ProviderId + select po; + + var query = from p in dbContext.Policies + join ou in dbContext.OrganizationUsers + on p.OrganizationId equals ou.OrganizationId + join o in dbContext.Organizations + on p.OrganizationId equals o.Id + where + p.Enabled && + o.Enabled && + o.UsePolicies && + ( + (ou.Status != OrganizationUserStatusType.Invited && ou.UserId == userId) || + // Invited orgUsers do not have a UserId associated with them, so we have to match up their email + (ou.Status == OrganizationUserStatusType.Invited && ou.Email == dbContext.Users.Find(userId).Email) + ) + select new PolicyDetails + { + OrganizationUserId = ou.Id, + OrganizationId = p.OrganizationId, + PolicyType = p.Type, + PolicyData = p.Data, + OrganizationUserType = ou.Type, + OrganizationUserStatus = ou.Status, + OrganizationUserPermissionsData = ou.Permissions, + IsProvider = providerOrganizations.Any(po => po.OrganizationId == p.OrganizationId) + }; + return await query.ToListAsync(); + } } diff --git a/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByUserId.sql b/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByUserId.sql new file mode 100644 index 0000000000..910ff3c4c6 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByUserId.sql @@ -0,0 +1,43 @@ +CREATE PROCEDURE [dbo].[PolicyDetails_ReadByUserId] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON +SELECT + OU.[Id] AS OrganizationUserId, + P.[OrganizationId], + P.[Type] AS PolicyType, + P.[Data] AS PolicyData, + OU.[Type] AS OrganizationUserType, + OU.[Status] AS OrganizationUserStatus, + OU.[Permissions] AS OrganizationUserPermissionsData, + CASE WHEN EXISTS ( + SELECT 1 + FROM [dbo].[ProviderUserView] PU + INNER JOIN [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId] + WHERE PU.[UserId] = OU.[UserId] AND PO.[OrganizationId] = P.[OrganizationId] + ) THEN 1 ELSE 0 END AS IsProvider +FROM [dbo].[PolicyView] P +INNER JOIN [dbo].[OrganizationUserView] OU + ON P.[OrganizationId] = OU.[OrganizationId] +INNER JOIN [dbo].[OrganizationView] O + ON P.[OrganizationId] = O.[Id] +WHERE + P.Enabled = 1 + AND O.Enabled = 1 + AND O.UsePolicies = 1 + AND ( + -- OrgUsers who have accepted their invite and are linked to a UserId + -- (Note: this excludes "invited but revoked" users who don't have an OU.UserId yet, + -- but those users will go through policy enforcement later as part of accepting their invite after being restored. + -- This is an intentionally unhandled edge case for now.) + (OU.[Status] != 0 AND OU.[UserId] = @UserId) + + -- 'Invited' OrgUsers are not linked to a UserId yet, so we have to look up their email + OR EXISTS ( + SELECT 1 + FROM [dbo].[UserView] U + WHERE U.[Id] = @UserId AND OU.[Email] = U.[Email] AND OU.[Status] = 0 + ) + ) +END diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs new file mode 100644 index 0000000000..4c98353774 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs @@ -0,0 +1,60 @@ +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.AdminConsole.Repositories; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies; + +[SutProviderCustomize] +public class PolicyRequirementQueryTests +{ + /// + /// Tests that the query correctly registers, retrieves and instantiates arbitrary IPolicyRequirements + /// according to their provided CreateRequirement delegate. + /// + [Theory, BitAutoData] + public async Task GetAsync_Works(Guid userId, Guid organizationId) + { + var policyRepository = Substitute.For(); + var factories = new List> + { + // In prod this cast is handled when the CreateRequirement delegate is registered in DI + (RequirementFactory)TestPolicyRequirement.Create + }; + + var sut = new PolicyRequirementQuery(policyRepository, factories); + policyRepository.GetPolicyDetailsByUserId(userId).Returns([ + new PolicyDetails + { + OrganizationId = organizationId + } + ]); + + var requirement = await sut.GetAsync(userId); + Assert.Equal(organizationId, requirement.OrganizationId); + } + + [Theory, BitAutoData] + public async Task GetAsync_ThrowsIfNoRequirementRegistered(Guid userId) + { + var policyRepository = Substitute.For(); + var sut = new PolicyRequirementQuery(policyRepository, []); + + var exception = await Assert.ThrowsAsync(() + => sut.GetAsync(userId)); + Assert.Contains("No Policy Requirement found", exception.Message); + } + + /// + /// Intentionally simplified PolicyRequirement that just holds the Policy.OrganizationId for us to assert against. + /// + private class TestPolicyRequirement : IPolicyRequirement + { + public Guid OrganizationId { get; init; } + public static TestPolicyRequirement Create(IEnumerable policyDetails) + => new() { OrganizationId = policyDetails.Single().OrganizationId }; + } +} diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs new file mode 100644 index 0000000000..07cb82dc02 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs @@ -0,0 +1,385 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Enums; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; +using Bit.Core.Utilities; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.PolicyRepository; + +public class GetPolicyDetailsByUserIdTests +{ + [DatabaseTheory, DatabaseData] + public async Task GetPolicyDetailsByUserId_NonInvitedUsers_Works( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + // OrgUser1 - owner of org1 - confirmed + var user = await userRepository.CreateTestUserAsync(); + var org1 = await CreateEnterpriseOrg(organizationRepository); + var orgUser1 = new OrganizationUser + { + OrganizationId = org1.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner, + Email = null // confirmed OrgUsers use the email on the User table + }; + await organizationUserRepository.CreateAsync(orgUser1); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org1.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + Data = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = true, IntSetting = 5 }) + }); + + // OrgUser2 - custom user of org2 - accepted + var org2 = await CreateEnterpriseOrg(organizationRepository); + var orgUser2 = new OrganizationUser + { + OrganizationId = org2.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Accepted, + Type = OrganizationUserType.Custom, + Email = null // accepted OrgUsers use the email on the User table + }; + orgUser2.SetPermissions(new Permissions + { + ManagePolicies = true + }); + await organizationUserRepository.CreateAsync(orgUser2); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org2.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + Data = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = false, IntSetting = 15 }) + }); + + // Act + var policyDetails = (await policyRepository.GetPolicyDetailsByUserId(user.Id)).ToList(); + + // Assert + Assert.Equal(2, policyDetails.Count); + + var actualPolicyDetails1 = policyDetails.Find(p => p.OrganizationUserId == orgUser1.Id); + var expectedPolicyDetails1 = new PolicyDetails + { + OrganizationUserId = orgUser1.Id, + OrganizationId = org1.Id, + PolicyType = PolicyType.SingleOrg, + PolicyData = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = true, IntSetting = 5 }), + OrganizationUserType = OrganizationUserType.Owner, + OrganizationUserStatus = OrganizationUserStatusType.Confirmed, + OrganizationUserPermissionsData = null, + IsProvider = false + }; + Assert.Equivalent(expectedPolicyDetails1, actualPolicyDetails1); + Assert.Equivalent(expectedPolicyDetails1.GetDataModel(), new TestPolicyData { BoolSetting = true, IntSetting = 5 }); + + var actualPolicyDetails2 = policyDetails.Find(p => p.OrganizationUserId == orgUser2.Id); + var expectedPolicyDetails2 = new PolicyDetails + { + OrganizationUserId = orgUser2.Id, + OrganizationId = org2.Id, + PolicyType = PolicyType.SingleOrg, + PolicyData = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = false, IntSetting = 15 }), + OrganizationUserType = OrganizationUserType.Custom, + OrganizationUserStatus = OrganizationUserStatusType.Accepted, + OrganizationUserPermissionsData = CoreHelpers.ClassToJsonData(new Permissions { ManagePolicies = true }), + IsProvider = false + }; + Assert.Equivalent(expectedPolicyDetails2, actualPolicyDetails2); + Assert.Equivalent(expectedPolicyDetails2.GetDataModel(), new TestPolicyData { BoolSetting = false, IntSetting = 15 }); + Assert.Equivalent(new Permissions { ManagePolicies = true }, actualPolicyDetails2.GetOrganizationUserCustomPermissions(), strict: true); + } + + [DatabaseTheory, DatabaseData] + public async Task GetPolicyDetailsByUserId_InvitedUser_Works( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await CreateEnterpriseOrg(organizationRepository); + var orgUser = new OrganizationUser + { + OrganizationId = org.Id, + UserId = null, // invited users have null userId + Status = OrganizationUserStatusType.Invited, + Type = OrganizationUserType.Custom, + Email = user.Email // invited users have matching Email + }; + await organizationUserRepository.CreateAsync(orgUser); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Act + var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); + + // Assert + var expectedPolicyDetails = new PolicyDetails + { + OrganizationUserId = orgUser.Id, + OrganizationId = org.Id, + PolicyType = PolicyType.SingleOrg, + OrganizationUserType = OrganizationUserType.Custom, + OrganizationUserStatus = OrganizationUserStatusType.Invited, + IsProvider = false + }; + + Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single()); + } + + [DatabaseTheory, DatabaseData] + public async Task GetPolicyDetailsByUserId_RevokedConfirmedUser_Works( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await CreateEnterpriseOrg(organizationRepository); + // User has been confirmed to the org but then revoked + var orgUser = new OrganizationUser + { + OrganizationId = org.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Revoked, + Type = OrganizationUserType.Owner, + Email = null + }; + await organizationUserRepository.CreateAsync(orgUser); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Act + var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); + + // Assert + var expectedPolicyDetails = new PolicyDetails + { + OrganizationUserId = orgUser.Id, + OrganizationId = org.Id, + PolicyType = PolicyType.SingleOrg, + OrganizationUserType = OrganizationUserType.Owner, + OrganizationUserStatus = OrganizationUserStatusType.Revoked, + IsProvider = false + }; + + Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single()); + } + + [DatabaseTheory, DatabaseData] + public async Task GetPolicyDetailsByUserId_RevokedInvitedUser_DoesntReturnPolicies( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await CreateEnterpriseOrg(organizationRepository); + // User has been invited to the org but then revoked - without ever being confirmed and linked to a user. + // This is an unhandled edge case because those users will go through policy enforcement later, + // as part of accepting their invite after being restored. For now this is just documented as expected behavior. + var orgUser = new OrganizationUser + { + OrganizationId = org.Id, + UserId = null, + Status = OrganizationUserStatusType.Revoked, + Type = OrganizationUserType.Owner, + Email = user.Email + }; + await organizationUserRepository.CreateAsync(orgUser); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Act + var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); + + Assert.Empty(actualPolicyDetails); + } + + [DatabaseTheory, DatabaseData] + public async Task GetPolicyDetailsByUserId_SetsIsProvider( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository, + IProviderRepository providerRepository, + IProviderUserRepository providerUserRepository, + IProviderOrganizationRepository providerOrganizationRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await CreateEnterpriseOrg(organizationRepository); + var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Arrange provider + var provider = await providerRepository.CreateAsync(new Provider + { + Name = Guid.NewGuid().ToString(), + Enabled = true + }); + await providerUserRepository.CreateAsync(new ProviderUser + { + ProviderId = provider.Id, + UserId = user.Id, + Status = ProviderUserStatusType.Confirmed + }); + await providerOrganizationRepository.CreateAsync(new ProviderOrganization + { + OrganizationId = org.Id, + ProviderId = provider.Id + }); + + // Act + var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); + + // Assert + var expectedPolicyDetails = new PolicyDetails + { + OrganizationUserId = orgUser.Id, + OrganizationId = org.Id, + PolicyType = PolicyType.SingleOrg, + OrganizationUserType = OrganizationUserType.Owner, + OrganizationUserStatus = OrganizationUserStatusType.Confirmed, + IsProvider = true + }; + + Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single()); + } + + [DatabaseTheory, DatabaseData] + public async Task GetPolicyDetailsByUserId_IgnoresDisabledOrganizations( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await CreateEnterpriseOrg(organizationRepository); + await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Org is disabled; its policies remain, but it is now inactive + org.Enabled = false; + await organizationRepository.ReplaceAsync(org); + + // Act + var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); + + // Assert + Assert.Empty(actualPolicyDetails); + } + + [DatabaseTheory, DatabaseData] + public async Task GetPolicyDetailsByUserId_IgnoresDowngradedOrganizations( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await CreateEnterpriseOrg(organizationRepository); + await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Org is downgraded; its policies remain but its plan no longer supports them + org.UsePolicies = false; + org.PlanType = PlanType.TeamsAnnually; + await organizationRepository.ReplaceAsync(org); + + // Act + var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); + + // Assert + Assert.Empty(actualPolicyDetails); + } + + [DatabaseTheory, DatabaseData] + public async Task GetPolicyDetailsByUserId_IgnoresDisabledPolicies( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await CreateEnterpriseOrg(organizationRepository); + await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = false, + Type = PolicyType.SingleOrg, + }); + + // Act + var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); + + // Assert + Assert.Empty(actualPolicyDetails); + } + + private class TestPolicyData : IPolicyDataModel + { + public bool BoolSetting { get; set; } + public int IntSetting { get; set; } + } + + private Task CreateEnterpriseOrg(IOrganizationRepository organizationRepository) + => organizationRepository.CreateAsync(new Organization + { + Name = Guid.NewGuid().ToString(), + BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL + Plan = "Test", // TODO: EF does not enforce this being NOT NULl + PlanType = PlanType.EnterpriseAnnually, + UsePolicies = true + }); +} diff --git a/util/Migrator/DbScripts/2025-02-14_00_PolicyDetails_ReadByUserId.sql b/util/Migrator/DbScripts/2025-02-14_00_PolicyDetails_ReadByUserId.sql new file mode 100644 index 0000000000..d50a092e18 --- /dev/null +++ b/util/Migrator/DbScripts/2025-02-14_00_PolicyDetails_ReadByUserId.sql @@ -0,0 +1,43 @@ +CREATE OR ALTER PROCEDURE [dbo].[PolicyDetails_ReadByUserId] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON +SELECT + OU.[Id] AS OrganizationUserId, + P.[OrganizationId], + P.[Type] AS PolicyType, + P.[Data] AS PolicyData, + OU.[Type] AS OrganizationUserType, + OU.[Status] AS OrganizationUserStatus, + OU.[Permissions] AS OrganizationUserPermissionsData, + CASE WHEN EXISTS ( + SELECT 1 + FROM [dbo].[ProviderUserView] PU + INNER JOIN [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId] + WHERE PU.[UserId] = OU.[UserId] AND PO.[OrganizationId] = P.[OrganizationId] + ) THEN 1 ELSE 0 END AS IsProvider +FROM [dbo].[PolicyView] P +INNER JOIN [dbo].[OrganizationUserView] OU + ON P.[OrganizationId] = OU.[OrganizationId] +INNER JOIN [dbo].[OrganizationView] O + ON P.[OrganizationId] = O.[Id] +WHERE + P.Enabled = 1 + AND O.Enabled = 1 + AND O.UsePolicies = 1 + AND ( + -- OrgUsers who have accepted their invite and are linked to a UserId + -- (Note: this excludes "invited but revoked" users who don't have an OU.UserId yet, + -- but those users will go through policy enforcement later as part of accepting their invite after being restored. + -- This is an intentionally unhandled edge case for now.) + (OU.[Status] != 0 AND OU.[UserId] = @UserId) + + -- 'Invited' OrgUsers are not linked to a UserId yet, so we have to look up their email + OR EXISTS ( + SELECT 1 + FROM [dbo].[UserView] U + WHERE U.[Id] = @UserId AND OU.[Email] = U.[Email] AND OU.[Status] = 0 + ) + ) +END From f4c37df883f2920036359a518028272ba62e442c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Fri, 14 Feb 2025 11:25:29 +0000 Subject: [PATCH 137/148] [PM-12490] Extract OrganizationService.EnableAsync into commands (#5321) * Add organization enable command implementation * Add unit tests for OrganizationEnableCommand * Add organization enable command registration for dependency injection * Refactor payment and subscription handlers to use IOrganizationEnableCommand for organization enabling * Remove EnableAsync methods from IOrganizationService and OrganizationService * Add xmldoc to IOrganizationEnableCommand * Refactor OrganizationEnableCommand to consolidate enable logic and add optional expiration --- .../PaymentSucceededHandler.cs | 11 +- .../SubscriptionUpdatedHandler.cs | 8 +- .../Interfaces/IOrganizationEnableCommand.cs | 11 ++ .../OrganizationEnableCommand.cs | 39 +++++ .../Services/IOrganizationService.cs | 2 - .../Implementations/OrganizationService.cs | 22 --- ...OrganizationServiceCollectionExtensions.cs | 4 + .../OrganizationEnableCommandTests.cs | 147 ++++++++++++++++++ 8 files changed, 213 insertions(+), 31 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationEnableCommand.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationEnableCommand.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationEnableCommandTests.cs diff --git a/src/Billing/Services/Implementations/PaymentSucceededHandler.cs b/src/Billing/Services/Implementations/PaymentSucceededHandler.cs index b16baea52e..1577e77c9e 100644 --- a/src/Billing/Services/Implementations/PaymentSucceededHandler.cs +++ b/src/Billing/Services/Implementations/PaymentSucceededHandler.cs @@ -1,4 +1,5 @@ using Bit.Billing.Constants; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Context; @@ -17,7 +18,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler { private readonly ILogger _logger; private readonly IStripeEventService _stripeEventService; - private readonly IOrganizationService _organizationService; private readonly IUserService _userService; private readonly IStripeFacade _stripeFacade; private readonly IProviderRepository _providerRepository; @@ -27,6 +27,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler private readonly IUserRepository _userRepository; private readonly IStripeEventUtilityService _stripeEventUtilityService; private readonly IPushNotificationService _pushNotificationService; + private readonly IOrganizationEnableCommand _organizationEnableCommand; public PaymentSucceededHandler( ILogger logger, @@ -39,8 +40,8 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler IUserRepository userRepository, IStripeEventUtilityService stripeEventUtilityService, IUserService userService, - IOrganizationService organizationService, - IPushNotificationService pushNotificationService) + IPushNotificationService pushNotificationService, + IOrganizationEnableCommand organizationEnableCommand) { _logger = logger; _stripeEventService = stripeEventService; @@ -52,8 +53,8 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler _userRepository = userRepository; _stripeEventUtilityService = stripeEventUtilityService; _userService = userService; - _organizationService = organizationService; _pushNotificationService = pushNotificationService; + _organizationEnableCommand = organizationEnableCommand; } /// @@ -142,7 +143,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler return; } - await _organizationService.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd); + await _organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd); var organization = await _organizationRepository.GetByIdAsync(organizationId.Value); await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); diff --git a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs index ea277a6307..10a1d1a186 100644 --- a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs @@ -1,6 +1,7 @@ using Bit.Billing.Constants; using Bit.Billing.Jobs; using Bit.Core; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Platform.Push; using Bit.Core.Repositories; @@ -24,6 +25,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler private readonly IOrganizationRepository _organizationRepository; private readonly ISchedulerFactory _schedulerFactory; private readonly IFeatureService _featureService; + private readonly IOrganizationEnableCommand _organizationEnableCommand; public SubscriptionUpdatedHandler( IStripeEventService stripeEventService, @@ -35,7 +37,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler IPushNotificationService pushNotificationService, IOrganizationRepository organizationRepository, ISchedulerFactory schedulerFactory, - IFeatureService featureService) + IFeatureService featureService, + IOrganizationEnableCommand organizationEnableCommand) { _stripeEventService = stripeEventService; _stripeEventUtilityService = stripeEventUtilityService; @@ -47,6 +50,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler _organizationRepository = organizationRepository; _schedulerFactory = schedulerFactory; _featureService = featureService; + _organizationEnableCommand = organizationEnableCommand; } /// @@ -90,7 +94,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler } case StripeSubscriptionStatus.Active when organizationId.HasValue: { - await _organizationService.EnableAsync(organizationId.Value); + await _organizationEnableCommand.EnableAsync(organizationId.Value); var organization = await _organizationRepository.GetByIdAsync(organizationId.Value); await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); break; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationEnableCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationEnableCommand.cs new file mode 100644 index 0000000000..522aa04a60 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationEnableCommand.cs @@ -0,0 +1,11 @@ +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; + +public interface IOrganizationEnableCommand +{ + /// + /// Enables an organization that is currently disabled and has a gateway configured. + /// + /// The unique identifier of the organization to enable. + /// When provided, sets the date the organization's subscription will expire. If not provided, no expiration date will be set. + Task EnableAsync(Guid organizationId, DateTime? expirationDate = null); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationEnableCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationEnableCommand.cs new file mode 100644 index 0000000000..660c792563 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationEnableCommand.cs @@ -0,0 +1,39 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; + +public class OrganizationEnableCommand : IOrganizationEnableCommand +{ + private readonly IApplicationCacheService _applicationCacheService; + private readonly IOrganizationRepository _organizationRepository; + + public OrganizationEnableCommand( + IApplicationCacheService applicationCacheService, + IOrganizationRepository organizationRepository) + { + _applicationCacheService = applicationCacheService; + _organizationRepository = organizationRepository; + } + + public async Task EnableAsync(Guid organizationId, DateTime? expirationDate = null) + { + var organization = await _organizationRepository.GetByIdAsync(organizationId); + if (organization is null || organization.Enabled || expirationDate is not null && organization.Gateway is null) + { + return; + } + + organization.Enabled = true; + + if (expirationDate is not null && organization.Gateway is not null) + { + organization.ExpirationDate = expirationDate; + organization.RevisionDate = DateTime.UtcNow; + } + + await _organizationRepository.ReplaceAsync(organization); + await _applicationCacheService.UpsertOrganizationAbilityAsync(organization); + } +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 7d73a3c903..683fbe9902 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -28,10 +28,8 @@ public interface IOrganizationService /// Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey, string privateKey); - Task EnableAsync(Guid organizationId, DateTime? expirationDate); Task DisableAsync(Guid organizationId, DateTime? expirationDate); Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate); - Task EnableAsync(Guid organizationId); Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated); Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type); Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type); diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 6f4aba4882..284c11cc78 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -686,18 +686,6 @@ public class OrganizationService : IOrganizationService } } - public async Task EnableAsync(Guid organizationId, DateTime? expirationDate) - { - var org = await GetOrgById(organizationId); - if (org != null && !org.Enabled && org.Gateway.HasValue) - { - org.Enabled = true; - org.ExpirationDate = expirationDate; - org.RevisionDate = DateTime.UtcNow; - await ReplaceAndUpdateCacheAsync(org); - } - } - public async Task DisableAsync(Guid organizationId, DateTime? expirationDate) { var org = await GetOrgById(organizationId); @@ -723,16 +711,6 @@ public class OrganizationService : IOrganizationService } } - public async Task EnableAsync(Guid organizationId) - { - var org = await GetOrgById(organizationId); - if (org != null && !org.Enabled) - { - org.Enabled = true; - await ReplaceAndUpdateCacheAsync(org); - } - } - public async Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated) { if (organization.Id == default(Guid)) diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 9d2e6e51e6..7db514887c 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -54,6 +54,7 @@ public static class OrganizationServiceCollectionExtensions services.AddOrganizationDomainCommandsQueries(); services.AddOrganizationSignUpCommands(); services.AddOrganizationDeleteCommands(); + services.AddOrganizationEnableCommands(); services.AddOrganizationAuthCommands(); services.AddOrganizationUserCommands(); services.AddOrganizationUserCommandsQueries(); @@ -69,6 +70,9 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); } + private static void AddOrganizationEnableCommands(this IServiceCollection services) => + services.AddScoped(); + private static void AddOrganizationConnectionCommands(this IServiceCollection services) { services.AddScoped(); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationEnableCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationEnableCommandTests.cs new file mode 100644 index 0000000000..6289c3b8e3 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationEnableCommandTests.cs @@ -0,0 +1,147 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations; + +[SutProviderCustomize] +public class OrganizationEnableCommandTests +{ + [Theory, BitAutoData] + public async Task EnableAsync_WhenOrganizationDoesNotExist_DoesNothing( + Guid organizationId, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(organizationId) + .Returns((Organization)null); + + await sutProvider.Sut.EnableAsync(organizationId); + + await sutProvider.GetDependency() + .DidNotReceive() + .ReplaceAsync(Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertOrganizationAbilityAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task EnableAsync_WhenOrganizationAlreadyEnabled_DoesNothing( + Organization organization, + SutProvider sutProvider) + { + organization.Enabled = true; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + await sutProvider.Sut.EnableAsync(organization.Id); + + await sutProvider.GetDependency() + .DidNotReceive() + .ReplaceAsync(Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertOrganizationAbilityAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task EnableAsync_WhenOrganizationDisabled_EnablesAndSaves( + Organization organization, + SutProvider sutProvider) + { + organization.Enabled = false; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + await sutProvider.Sut.EnableAsync(organization.Id); + + Assert.True(organization.Enabled); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(organization); + await sutProvider.GetDependency() + .Received(1) + .UpsertOrganizationAbilityAsync(organization); + } + + [Theory, BitAutoData] + public async Task EnableAsync_WithExpiration_WhenOrganizationHasNoGateway_DoesNothing( + Organization organization, + DateTime expirationDate, + SutProvider sutProvider) + { + organization.Enabled = false; + organization.Gateway = null; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + await sutProvider.Sut.EnableAsync(organization.Id, expirationDate); + + await sutProvider.GetDependency() + .DidNotReceive() + .ReplaceAsync(Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertOrganizationAbilityAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task EnableAsync_WithExpiration_WhenValid_EnablesAndSetsExpiration( + Organization organization, + DateTime expirationDate, + SutProvider sutProvider) + { + organization.Enabled = false; + organization.Gateway = GatewayType.Stripe; + organization.RevisionDate = DateTime.UtcNow.AddDays(-1); + var originalRevisionDate = organization.RevisionDate; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + await sutProvider.Sut.EnableAsync(organization.Id, expirationDate); + + Assert.True(organization.Enabled); + Assert.Equal(expirationDate, organization.ExpirationDate); + Assert.True(organization.RevisionDate > originalRevisionDate); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(organization); + await sutProvider.GetDependency() + .Received(1) + .UpsertOrganizationAbilityAsync(organization); + } + + [Theory, BitAutoData] + public async Task EnableAsync_WithoutExpiration_DoesNotUpdateRevisionDate( + Organization organization, + SutProvider sutProvider) + { + organization.Enabled = false; + var originalRevisionDate = organization.RevisionDate; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + await sutProvider.Sut.EnableAsync(organization.Id); + + Assert.True(organization.Enabled); + Assert.Equal(originalRevisionDate, organization.RevisionDate); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(organization); + await sutProvider.GetDependency() + .Received(1) + .UpsertOrganizationAbilityAsync(organization); + } +} From 762acdbd0371b1470d59d44f625fb6bb519560e4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 17:37:34 +0100 Subject: [PATCH 138/148] [deps] Tools: Update MailKit to 4.10.0 (#5408) 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index e2aaa9aa23..860cf33298 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -42,7 +42,7 @@ - + From 288f08da2ad26086b58e8145129b91ed86cd6ea7 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Fri, 14 Feb 2025 18:01:49 +0100 Subject: [PATCH 139/148] =?UTF-8?q?[PM-18268]=20SM=20Marketing=20Initiated?= =?UTF-8?q?=20Trials=20cause=20invoice=20previewing=20to=20=E2=80=A6=20(#5?= =?UTF-8?q?404)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Implementations/StripePaymentService.cs | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index fb5c7364a5..4813608fb5 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -1852,7 +1852,6 @@ public class StripePaymentService : IPaymentService Enabled = true, }, Currency = "usd", - Discounts = new List(), SubscriptionDetails = new InvoiceSubscriptionDetailsOptions { Items = @@ -1903,29 +1902,23 @@ public class StripePaymentService : IPaymentService ]; } - if (gatewayCustomerId != null) + if (!string.IsNullOrWhiteSpace(gatewayCustomerId)) { var gatewayCustomer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId); if (gatewayCustomer.Discount != null) { - options.Discounts.Add(new InvoiceDiscountOptions - { - Discount = gatewayCustomer.Discount.Id - }); + options.Coupon = gatewayCustomer.Discount.Coupon.Id; } + } - if (gatewaySubscriptionId != null) + if (!string.IsNullOrWhiteSpace(gatewaySubscriptionId)) + { + var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId); + + if (gatewaySubscription?.Discount != null) { - var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId); - - if (gatewaySubscription?.Discount != null) - { - options.Discounts.Add(new InvoiceDiscountOptions - { - Discount = gatewaySubscription.Discount.Id - }); - } + options.Coupon ??= gatewaySubscription.Discount.Coupon.Id; } } @@ -1976,7 +1969,6 @@ public class StripePaymentService : IPaymentService Enabled = true, }, Currency = "usd", - Discounts = new List(), SubscriptionDetails = new InvoiceSubscriptionDetailsOptions { Items = @@ -2069,7 +2061,7 @@ public class StripePaymentService : IPaymentService if (gatewayCustomer.Discount != null) { - options.Discounts.Add(new InvoiceDiscountOptions { Discount = gatewayCustomer.Discount.Id }); + options.Coupon = gatewayCustomer.Discount.Coupon.Id; } } @@ -2079,10 +2071,7 @@ public class StripePaymentService : IPaymentService if (gatewaySubscription?.Discount != null) { - options.Discounts.Add(new InvoiceDiscountOptions - { - Discount = gatewaySubscription.Discount.Id - }); + options.Coupon ??= gatewaySubscription.Discount.Coupon.Id; } } From 5709ea36f4d174049704ac83669158fa5a073b68 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Fri, 14 Feb 2025 12:03:09 -0500 Subject: [PATCH 140/148] [PM-15485] Add provider plan details to provider Admin pages (#5326) * Add Provider Plan details to Provider Admin pages * Run dotnet format * Thomas' feedback * Updated code ownership * Robert's feedback * Thomas' feedback --- src/Admin/Admin.csproj | 1 - .../Controllers/ProvidersController.cs | 3 +- .../AdminConsole/Models/ProviderEditModel.cs | 2 +- .../AdminConsole/Models/ProviderViewModel.cs | 49 +++++++++++++++++-- .../AdminConsole/Views/Providers/Edit.cshtml | 4 ++ .../AdminConsole/Views/Providers/View.cshtml | 4 ++ .../Billing/Models/ProviderPlanViewModel.cs | 26 ++++++++++ .../Views/Providers/ProviderPlans.cshtml | 18 +++++++ ...ProviderOrganizationOrganizationDetails.cs | 2 + ...rganizationDetailsReadByProviderIdQuery.cs | 1 + ...derOrganizationOrganizationDetailsView.sql | 1 + ...derOrganizationOrganizationDetailsView.sql | 23 +++++++++ 12 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 src/Admin/Billing/Models/ProviderPlanViewModel.cs create mode 100644 src/Admin/Billing/Views/Providers/ProviderPlans.cshtml create mode 100644 util/Migrator/DbScripts/2025-01-29_00_AddPlanTypeToProviderOrganizationOrganizationDetailsView.sql diff --git a/src/Admin/Admin.csproj b/src/Admin/Admin.csproj index 5493e65afd..4a255eefb2 100644 --- a/src/Admin/Admin.csproj +++ b/src/Admin/Admin.csproj @@ -16,7 +16,6 @@ - diff --git a/src/Admin/AdminConsole/Controllers/ProvidersController.cs b/src/Admin/AdminConsole/Controllers/ProvidersController.cs index 8a56483a60..6229a4deab 100644 --- a/src/Admin/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Admin/AdminConsole/Controllers/ProvidersController.cs @@ -235,7 +235,8 @@ public class ProvidersController : Controller var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id); var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id); - return View(new ProviderViewModel(provider, users, providerOrganizations)); + var providerPlans = await _providerPlanRepository.GetByProviderId(id); + return View(new ProviderViewModel(provider, users, providerOrganizations, providerPlans.ToList())); } [SelfHosted(NotSelfHostedOnly = true)] diff --git a/src/Admin/AdminConsole/Models/ProviderEditModel.cs b/src/Admin/AdminConsole/Models/ProviderEditModel.cs index 7fd5c765c8..bcdf602c07 100644 --- a/src/Admin/AdminConsole/Models/ProviderEditModel.cs +++ b/src/Admin/AdminConsole/Models/ProviderEditModel.cs @@ -19,7 +19,7 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject IEnumerable organizations, IReadOnlyCollection providerPlans, string gatewayCustomerUrl = null, - string gatewaySubscriptionUrl = null) : base(provider, providerUsers, organizations) + string gatewaySubscriptionUrl = null) : base(provider, providerUsers, organizations, providerPlans) { Name = provider.DisplayName(); BusinessName = provider.DisplayBusinessName(); diff --git a/src/Admin/AdminConsole/Models/ProviderViewModel.cs b/src/Admin/AdminConsole/Models/ProviderViewModel.cs index 9c4d07e8bf..724e6220b3 100644 --- a/src/Admin/AdminConsole/Models/ProviderViewModel.cs +++ b/src/Admin/AdminConsole/Models/ProviderViewModel.cs @@ -1,6 +1,9 @@ -using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Admin.Billing.Models; +using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; +using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Enums; namespace Bit.Admin.AdminConsole.Models; @@ -8,17 +11,57 @@ public class ProviderViewModel { public ProviderViewModel() { } - public ProviderViewModel(Provider provider, IEnumerable providerUsers, IEnumerable organizations) + public ProviderViewModel( + Provider provider, + IEnumerable providerUsers, + IEnumerable organizations, + IReadOnlyCollection providerPlans) { Provider = provider; UserCount = providerUsers.Count(); ProviderAdmins = providerUsers.Where(u => u.Type == ProviderUserType.ProviderAdmin); - ProviderOrganizations = organizations.Where(o => o.ProviderId == provider.Id); + + if (Provider.Type == ProviderType.Msp) + { + var usedTeamsSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.TeamsMonthly) + .Sum(po => po.OccupiedSeats) ?? 0; + var teamsProviderPlan = providerPlans.FirstOrDefault(plan => plan.PlanType == PlanType.TeamsMonthly); + if (teamsProviderPlan != null && teamsProviderPlan.IsConfigured()) + { + ProviderPlanViewModels.Add(new ProviderPlanViewModel("Teams (Monthly) Subscription", teamsProviderPlan, usedTeamsSeats)); + } + + var usedEnterpriseSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.EnterpriseMonthly) + .Sum(po => po.OccupiedSeats) ?? 0; + var enterpriseProviderPlan = providerPlans.FirstOrDefault(plan => plan.PlanType == PlanType.EnterpriseMonthly); + if (enterpriseProviderPlan != null && enterpriseProviderPlan.IsConfigured()) + { + ProviderPlanViewModels.Add(new ProviderPlanViewModel("Enterprise (Monthly) Subscription", enterpriseProviderPlan, usedEnterpriseSeats)); + } + } + else if (Provider.Type == ProviderType.MultiOrganizationEnterprise) + { + var usedEnterpriseSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.EnterpriseMonthly) + .Sum(po => po.OccupiedSeats).GetValueOrDefault(0); + var enterpriseProviderPlan = providerPlans.FirstOrDefault(); + if (enterpriseProviderPlan != null && enterpriseProviderPlan.IsConfigured()) + { + var planLabel = enterpriseProviderPlan.PlanType switch + { + PlanType.EnterpriseMonthly => "Enterprise (Monthly) Subscription", + PlanType.EnterpriseAnnually => "Enterprise (Annually) Subscription", + _ => string.Empty + }; + + ProviderPlanViewModels.Add(new ProviderPlanViewModel(planLabel, enterpriseProviderPlan, usedEnterpriseSeats)); + } + } } public int UserCount { get; set; } public Provider Provider { get; set; } public IEnumerable ProviderAdmins { get; set; } public IEnumerable ProviderOrganizations { get; set; } + public List ProviderPlanViewModels { get; set; } = []; } diff --git a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml index 43d72338be..be13a7c740 100644 --- a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml @@ -17,6 +17,10 @@

Provider Information

@await Html.PartialAsync("_ViewInformation", Model) +@if (Model.ProviderPlanViewModels.Any()) +{ + @await Html.PartialAsync("~/Billing/Views/Providers/ProviderPlans.cshtml", Model.ProviderPlanViewModels) +} @await Html.PartialAsync("Admins", Model)
diff --git a/src/Admin/AdminConsole/Views/Providers/View.cshtml b/src/Admin/AdminConsole/Views/Providers/View.cshtml index 0ae31627fc..0774ee2f70 100644 --- a/src/Admin/AdminConsole/Views/Providers/View.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/View.cshtml @@ -7,5 +7,9 @@

Information

@await Html.PartialAsync("_ViewInformation", Model) +@if (Model.ProviderPlanViewModels.Any()) +{ + @await Html.PartialAsync("ProviderPlans", Model.ProviderPlanViewModels) +} @await Html.PartialAsync("Admins", Model) @await Html.PartialAsync("Organizations", Model) diff --git a/src/Admin/Billing/Models/ProviderPlanViewModel.cs b/src/Admin/Billing/Models/ProviderPlanViewModel.cs new file mode 100644 index 0000000000..7a50aba286 --- /dev/null +++ b/src/Admin/Billing/Models/ProviderPlanViewModel.cs @@ -0,0 +1,26 @@ +using Bit.Core.Billing.Entities; + +namespace Bit.Admin.Billing.Models; + +public class ProviderPlanViewModel +{ + public string Name { get; set; } + public int PurchasedSeats { get; set; } + public int AssignedSeats { get; set; } + public int UsedSeats { get; set; } + public int RemainingSeats { get; set; } + + public ProviderPlanViewModel( + string name, + ProviderPlan providerPlan, + int usedSeats) + { + var purchasedSeats = (providerPlan.SeatMinimum ?? 0) + (providerPlan.PurchasedSeats ?? 0); + + Name = name; + PurchasedSeats = purchasedSeats; + AssignedSeats = providerPlan.AllocatedSeats ?? 0; + UsedSeats = usedSeats; + RemainingSeats = purchasedSeats - AssignedSeats; + } +} diff --git a/src/Admin/Billing/Views/Providers/ProviderPlans.cshtml b/src/Admin/Billing/Views/Providers/ProviderPlans.cshtml new file mode 100644 index 0000000000..e84f5a2779 --- /dev/null +++ b/src/Admin/Billing/Views/Providers/ProviderPlans.cshtml @@ -0,0 +1,18 @@ +@model List +@foreach (var plan in Model) +{ +

@plan.Name

+
+
Purchased Seats
+
@plan.PurchasedSeats
+ +
Assigned Seats
+
@plan.AssignedSeats
+ +
Used Seats
+
@plan.UsedSeats
+ +
Remaining Seats
+
@plan.RemainingSeats
+
+} diff --git a/src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationOrganizationDetails.cs index 1b2112707c..9d84f60c4c 100644 --- a/src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationOrganizationDetails.cs @@ -1,5 +1,6 @@ using System.Net; using System.Text.Json.Serialization; +using Bit.Core.Billing.Enums; using Bit.Core.Enums; using Bit.Core.Utilities; @@ -23,6 +24,7 @@ public class ProviderOrganizationOrganizationDetails public int? OccupiedSeats { get; set; } public int? Seats { get; set; } public string Plan { get; set; } + public PlanType PlanType { get; set; } public OrganizationStatusType Status { get; set; } /// diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderOrganizationOrganizationDetailsReadByProviderIdQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderOrganizationOrganizationDetailsReadByProviderIdQuery.cs index 62e46566d7..4f99391a24 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderOrganizationOrganizationDetailsReadByProviderIdQuery.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderOrganizationOrganizationDetailsReadByProviderIdQuery.cs @@ -35,6 +35,7 @@ public class ProviderOrganizationOrganizationDetailsReadByProviderIdQuery : IQue OccupiedSeats = x.o.OrganizationUsers.Count(ou => ou.Status >= 0), Seats = x.o.Seats, Plan = x.o.Plan, + PlanType = x.o.PlanType, Status = x.o.Status }); } diff --git a/src/Sql/dbo/Views/ProviderOrganizationOrganizationDetailsView.sql b/src/Sql/dbo/Views/ProviderOrganizationOrganizationDetailsView.sql index 0fcff73699..3a08418ed3 100644 --- a/src/Sql/dbo/Views/ProviderOrganizationOrganizationDetailsView.sql +++ b/src/Sql/dbo/Views/ProviderOrganizationOrganizationDetailsView.sql @@ -13,6 +13,7 @@ SELECT (SELECT COUNT(1) FROM [dbo].[OrganizationUser] OU WHERE OU.OrganizationId = PO.OrganizationId AND OU.Status >= 0) OccupiedSeats, O.[Seats], O.[Plan], + O.[PlanType], O.[Status] FROM [dbo].[ProviderOrganization] PO diff --git a/util/Migrator/DbScripts/2025-01-29_00_AddPlanTypeToProviderOrganizationOrganizationDetailsView.sql b/util/Migrator/DbScripts/2025-01-29_00_AddPlanTypeToProviderOrganizationOrganizationDetailsView.sql new file mode 100644 index 0000000000..df4c145b71 --- /dev/null +++ b/util/Migrator/DbScripts/2025-01-29_00_AddPlanTypeToProviderOrganizationOrganizationDetailsView.sql @@ -0,0 +1,23 @@ +-- Add column 'PlanType' +CREATE OR AlTER VIEW [dbo].[ProviderOrganizationOrganizationDetailsView] +AS +SELECT + PO.[Id], + PO.[ProviderId], + PO.[OrganizationId], + O.[Name] OrganizationName, + PO.[Key], + PO.[Settings], + PO.[CreationDate], + PO.[RevisionDate], + (SELECT COUNT(1) FROM [dbo].[OrganizationUser] OU WHERE OU.OrganizationId = PO.OrganizationId AND OU.Status = 2) UserCount, + (SELECT COUNT(1) FROM [dbo].[OrganizationUser] OU WHERE OU.OrganizationId = PO.OrganizationId AND OU.Status >= 0) OccupiedSeats, + O.[Seats], + O.[Plan], + O.[PlanType], + O.[Status] +FROM + [dbo].[ProviderOrganization] PO + LEFT JOIN + [dbo].[Organization] O ON O.[Id] = PO.[OrganizationId] +GO From f80acaec0a9bea60aadf44b18ed77a838ea4be28 Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Fri, 14 Feb 2025 13:38:27 -0500 Subject: [PATCH 141/148] [PM-17562] Refactor to Support Multiple Message Payloads (#5400) * [PM-17562] Refactor to Support Multiple Message Payloads * Change signature as per PR suggestion --- .../Services/IEventMessageHandler.cs | 2 ++ .../AzureServiceBusEventListenerService.cs | 18 +++++++++++--- .../AzureServiceBusEventWriteService.cs | 8 ++++--- .../AzureTableStorageEventHandler.cs | 5 ++++ .../Implementations/EventRepositoryHandler.cs | 5 ++++ .../RabbitMqEventListenerService.cs | 19 ++++++++++++--- .../RabbitMqEventWriteService.cs | 7 ++---- .../Implementations/WebhookEventHandler.cs | 24 ++++++++++--------- src/Events/Startup.cs | 23 ++++++++++++++++-- .../Services/EventRepositoryHandlerTests.cs | 11 +++++++++ .../Services/WebhookEventHandlerTests.cs | 23 ++++++++++++++++-- 11 files changed, 116 insertions(+), 29 deletions(-) diff --git a/src/Core/AdminConsole/Services/IEventMessageHandler.cs b/src/Core/AdminConsole/Services/IEventMessageHandler.cs index 5df9544c29..83c5e33ecb 100644 --- a/src/Core/AdminConsole/Services/IEventMessageHandler.cs +++ b/src/Core/AdminConsole/Services/IEventMessageHandler.cs @@ -5,4 +5,6 @@ namespace Bit.Core.Services; public interface IEventMessageHandler { Task HandleEventAsync(EventMessage eventMessage); + + Task HandleManyEventsAsync(IEnumerable eventMessages); } diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs index 5c329ce8ad..4cd71ae77e 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Text; +using System.Text.Json; using Azure.Messaging.ServiceBus; using Bit.Core.Models.Data; using Bit.Core.Settings; @@ -29,9 +30,20 @@ public class AzureServiceBusEventListenerService : EventLoggingListenerService { try { - var eventMessage = JsonSerializer.Deserialize(args.Message.Body.ToString()); + using var jsonDocument = JsonDocument.Parse(Encoding.UTF8.GetString(args.Message.Body)); + var root = jsonDocument.RootElement; - await _handler.HandleEventAsync(eventMessage); + if (root.ValueKind == JsonValueKind.Array) + { + var eventMessages = root.Deserialize>(); + await _handler.HandleManyEventsAsync(eventMessages); + } + else if (root.ValueKind == JsonValueKind.Object) + { + var eventMessage = root.Deserialize(); + await _handler.HandleEventAsync(eventMessage); + + } await args.CompleteMessageAsync(args.Message); } catch (Exception exception) diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs index ed8f45ed55..fc865b327c 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs @@ -29,10 +29,12 @@ public class AzureServiceBusEventWriteService : IEventWriteService, IAsyncDispos public async Task CreateManyAsync(IEnumerable events) { - foreach (var e in events) + var message = new ServiceBusMessage(JsonSerializer.SerializeToUtf8Bytes(events)) { - await CreateAsync(e); - } + ContentType = "application/json" + }; + + await _sender.SendMessageAsync(message); } public async ValueTask DisposeAsync() diff --git a/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs index 2612ba0487..aa545913b1 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs @@ -11,4 +11,9 @@ public class AzureTableStorageEventHandler( { return eventWriteService.CreateManyAsync(EventTableEntity.IndexEvent(eventMessage)); } + + public Task HandleManyEventsAsync(IEnumerable eventMessages) + { + return eventWriteService.CreateManyAsync(eventMessages.SelectMany(EventTableEntity.IndexEvent)); + } } diff --git a/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs index 6e4158122c..ee3a2d5db2 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs @@ -11,4 +11,9 @@ public class EventRepositoryHandler( { return eventWriteService.CreateAsync(eventMessage); } + + public Task HandleManyEventsAsync(IEnumerable eventMessages) + { + return eventWriteService.CreateManyAsync(eventMessages); + } } diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs index c302497142..1ee3fa5ea7 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Text; +using System.Text.Json; using Bit.Core.Models.Data; using Bit.Core.Settings; using Microsoft.Extensions.Logging; @@ -62,8 +63,20 @@ public class RabbitMqEventListenerService : EventLoggingListenerService { try { - var eventMessage = JsonSerializer.Deserialize(eventArgs.Body.Span); - await _handler.HandleEventAsync(eventMessage); + using var jsonDocument = JsonDocument.Parse(Encoding.UTF8.GetString(eventArgs.Body.Span)); + var root = jsonDocument.RootElement; + + if (root.ValueKind == JsonValueKind.Array) + { + var eventMessages = root.Deserialize>(); + await _handler.HandleManyEventsAsync(eventMessages); + } + else if (root.ValueKind == JsonValueKind.Object) + { + var eventMessage = root.Deserialize(); + await _handler.HandleEventAsync(eventMessage); + + } } catch (Exception ex) { diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs index d89cf890ac..86abddec58 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs @@ -41,12 +41,9 @@ public class RabbitMqEventWriteService : IEventWriteService, IAsyncDisposable using var channel = await connection.CreateChannelAsync(); await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true); - foreach (var e in events) - { - var body = JsonSerializer.SerializeToUtf8Bytes(e); + var body = JsonSerializer.SerializeToUtf8Bytes(events); - await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: string.Empty, body: body); - } + await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: string.Empty, body: body); } public async ValueTask DisposeAsync() diff --git a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs index 60abc198d8..d152f9011b 100644 --- a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs @@ -4,25 +4,27 @@ using Bit.Core.Settings; namespace Bit.Core.Services; -public class WebhookEventHandler : IEventMessageHandler +public class WebhookEventHandler( + IHttpClientFactory httpClientFactory, + GlobalSettings globalSettings) + : IEventMessageHandler { - private readonly HttpClient _httpClient; - private readonly string _webhookUrl; + private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName); + private readonly string _webhookUrl = globalSettings.EventLogging.WebhookUrl; public const string HttpClientName = "WebhookEventHandlerHttpClient"; - public WebhookEventHandler( - IHttpClientFactory httpClientFactory, - GlobalSettings globalSettings) - { - _httpClient = httpClientFactory.CreateClient(HttpClientName); - _webhookUrl = globalSettings.EventLogging.WebhookUrl; - } - public async Task HandleEventAsync(EventMessage eventMessage) { var content = JsonContent.Create(eventMessage); var response = await _httpClient.PostAsync(_webhookUrl, content); response.EnsureSuccessStatusCode(); } + + public async Task HandleManyEventsAsync(IEnumerable eventMessages) + { + var content = JsonContent.Create(eventMessages); + var response = await _httpClient.PostAsync(_webhookUrl, content); + response.EnsureSuccessStatusCode(); + } } diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index 431f449708..57af285b03 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -1,4 +1,5 @@ using System.Globalization; +using Bit.Core.AdminConsole.Services.Implementations; using Bit.Core.Context; using Bit.Core.IdentityServer; using Bit.Core.Services; @@ -63,11 +64,29 @@ public class Startup services.AddScoped(); if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString)) { - services.AddSingleton(); + if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) + { + services.AddSingleton(); + } + else + { + services.AddSingleton(); + } } else { - services.AddSingleton(); + if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName)) + { + services.AddSingleton(); + } + else + { + services.AddSingleton(); + } } services.AddOptionality(); diff --git a/test/Core.Test/AdminConsole/Services/EventRepositoryHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventRepositoryHandlerTests.cs index 2b143f5cb8..48c3a143d4 100644 --- a/test/Core.Test/AdminConsole/Services/EventRepositoryHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventRepositoryHandlerTests.cs @@ -21,4 +21,15 @@ public class EventRepositoryHandlerTests Arg.Is(AssertHelper.AssertPropertyEqual(eventMessage)) ); } + + [Theory, BitAutoData] + public async Task HandleManyEventAsync_WritesEventsToIEventWriteService( + IEnumerable eventMessages, + SutProvider sutProvider) + { + await sutProvider.Sut.HandleManyEventsAsync(eventMessages); + await sutProvider.GetDependency().Received(1).CreateManyAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(eventMessages)) + ); + } } diff --git a/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs b/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs index eab0be88a1..6c7d7178c1 100644 --- a/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs @@ -44,10 +44,9 @@ public class WebhookEventHandlerTests } [Theory, BitAutoData] - public async Task HandleEventAsync_PostsEventsToUrl(EventMessage eventMessage) + public async Task HandleEventAsync_PostsEventToUrl(EventMessage eventMessage) { var sutProvider = GetSutProvider(); - var content = JsonContent.Create(eventMessage); await sutProvider.Sut.HandleEventAsync(eventMessage); sutProvider.GetDependency().Received(1).CreateClient( @@ -63,4 +62,24 @@ public class WebhookEventHandlerTests Assert.Equal(_webhookUrl, request.RequestUri.ToString()); AssertHelper.AssertPropertyEqual(eventMessage, returned, new[] { "IdempotencyId" }); } + + [Theory, BitAutoData] + public async Task HandleEventManyAsync_PostsEventsToUrl(IEnumerable eventMessages) + { + var sutProvider = GetSutProvider(); + + await sutProvider.Sut.HandleManyEventsAsync(eventMessages); + sutProvider.GetDependency().Received(1).CreateClient( + Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) + ); + + Assert.Single(_handler.CapturedRequests); + var request = _handler.CapturedRequests[0]; + Assert.NotNull(request); + var returned = request.Content.ReadFromJsonAsAsyncEnumerable(); + + Assert.Equal(HttpMethod.Post, request.Method); + Assert.Equal(_webhookUrl, request.RequestUri.ToString()); + AssertHelper.AssertPropertyEqual(eventMessages, returned, new[] { "IdempotencyId" }); + } } From ac443ed495ba087e04fc470163b2180f6b28f6f8 Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Tue, 18 Feb 2025 09:53:49 -0500 Subject: [PATCH 142/148] [pm-13985] Add a cancel endpoint to prevent authorization errors (#5229) --- .../AdminConsole/Controllers/ProvidersController.cs | 12 ++++++++++++ .../Views/Providers/CreateOrganization.cshtml | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Admin/AdminConsole/Controllers/ProvidersController.cs b/src/Admin/AdminConsole/Controllers/ProvidersController.cs index 6229a4deab..38e25939c4 100644 --- a/src/Admin/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Admin/AdminConsole/Controllers/ProvidersController.cs @@ -251,6 +251,18 @@ public class ProvidersController : Controller return View(provider); } + [SelfHosted(NotSelfHostedOnly = true)] + public async Task Cancel(Guid id) + { + var provider = await GetEditModel(id); + if (provider == null) + { + return RedirectToAction("Index"); + } + + return RedirectToAction("Edit", new { id }); + } + [HttpPost] [ValidateAntiForgeryToken] [SelfHosted(NotSelfHostedOnly = true)] diff --git a/src/Admin/AdminConsole/Views/Providers/CreateOrganization.cshtml b/src/Admin/AdminConsole/Views/Providers/CreateOrganization.cshtml index 6b7ccbdb12..eb790f20ba 100644 --- a/src/Admin/AdminConsole/Views/Providers/CreateOrganization.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/CreateOrganization.cshtml @@ -19,8 +19,8 @@
- +
From 055e4e3066593b386d18b01fca632e3af2ade50c Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:42:00 -0500 Subject: [PATCH 143/148] Add RequestDeviceIdentifier to response (#5403) --- src/Api/Auth/Models/Response/AuthRequestResponseModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs b/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs index 3a07873451..50f7f5a3e7 100644 --- a/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs +++ b/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs @@ -18,6 +18,7 @@ public class AuthRequestResponseModel : ResponseModel Id = authRequest.Id; PublicKey = authRequest.PublicKey; + RequestDeviceIdentifier = authRequest.RequestDeviceIdentifier; RequestDeviceTypeValue = authRequest.RequestDeviceType; RequestDeviceType = authRequest.RequestDeviceType.GetType().GetMember(authRequest.RequestDeviceType.ToString()) .FirstOrDefault()?.GetCustomAttribute()?.GetName(); @@ -32,6 +33,7 @@ public class AuthRequestResponseModel : ResponseModel public Guid Id { get; set; } public string PublicKey { get; set; } + public string RequestDeviceIdentifier { get; set; } public DeviceType RequestDeviceTypeValue { get; set; } public string RequestDeviceType { get; set; } public string RequestIpAddress { get; set; } From f27886e3124976669515e239da9d8d1f9c3baeb8 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 18 Feb 2025 12:54:11 -0500 Subject: [PATCH 144/148] [PM-17932] Convert Renovate config to JSON5 (#5414) * Migrated Renovate config to JSON5 * Apply Prettier * Added comment for demonstration --------- Co-authored-by: Matt Bishop --- .github/renovate.json | 199 ----------------------------------------- .github/renovate.json5 | 199 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+), 199 deletions(-) delete mode 100644 .github/renovate.json create mode 100644 .github/renovate.json5 diff --git a/.github/renovate.json b/.github/renovate.json deleted file mode 100644 index 31d78a4d4e..0000000000 --- a/.github/renovate.json +++ /dev/null @@ -1,199 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["github>bitwarden/renovate-config"], - "enabledManagers": [ - "dockerfile", - "docker-compose", - "github-actions", - "npm", - "nuget" - ], - "packageRules": [ - { - "groupName": "dockerfile minor", - "matchManagers": ["dockerfile"], - "matchUpdateTypes": ["minor"] - }, - { - "groupName": "docker-compose minor", - "matchManagers": ["docker-compose"], - "matchUpdateTypes": ["minor"] - }, - { - "groupName": "github-action minor", - "matchManagers": ["github-actions"], - "matchUpdateTypes": ["minor"] - }, - { - "matchManagers": ["dockerfile", "docker-compose"], - "commitMessagePrefix": "[deps] BRE:" - }, - { - "matchPackageNames": ["DnsClient"], - "description": "Admin Console owned dependencies", - "commitMessagePrefix": "[deps] AC:", - "reviewers": ["team:team-admin-console-dev"] - }, - { - "matchFileNames": ["src/Admin/package.json", "src/Sso/package.json"], - "description": "Admin & SSO npm packages", - "commitMessagePrefix": "[deps] Auth:", - "reviewers": ["team:team-auth-dev"] - }, - { - "matchPackageNames": [ - "Azure.Extensions.AspNetCore.DataProtection.Blobs", - "DuoUniversal", - "Fido2.AspNet", - "Duende.IdentityServer", - "Microsoft.Extensions.Identity.Stores", - "Otp.NET", - "Sustainsys.Saml2.AspNetCore2", - "YubicoDotNetClient" - ], - "description": "Auth owned dependencies", - "commitMessagePrefix": "[deps] Auth:", - "reviewers": ["team:team-auth-dev"] - }, - { - "matchPackageNames": [ - "AutoFixture.AutoNSubstitute", - "AutoFixture.Xunit2", - "BenchmarkDotNet", - "BitPay.Light", - "Braintree", - "coverlet.collector", - "CsvHelper", - "Kralizek.AutoFixture.Extensions.MockHttp", - "Microsoft.AspNetCore.Mvc.Testing", - "Microsoft.Extensions.Logging", - "Microsoft.Extensions.Logging.Console", - "Newtonsoft.Json", - "NSubstitute", - "Sentry.Serilog", - "Serilog.AspNetCore", - "Serilog.Extensions.Logging", - "Serilog.Extensions.Logging.File", - "Serilog.Sinks.AzureCosmosDB", - "Serilog.Sinks.SyslogMessages", - "Stripe.net", - "Swashbuckle.AspNetCore", - "Swashbuckle.AspNetCore.SwaggerGen", - "xunit", - "xunit.runner.visualstudio" - ], - "description": "Billing owned dependencies", - "commitMessagePrefix": "[deps] Billing:", - "reviewers": ["team:team-billing-dev"] - }, - { - "matchPackagePatterns": ["^Microsoft.Extensions.Logging"], - "groupName": "Microsoft.Extensions.Logging", - "description": "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset" - }, - { - "matchPackageNames": [ - "Dapper", - "dbup-sqlserver", - "dotnet-ef", - "linq2db.EntityFrameworkCore", - "Microsoft.Azure.Cosmos", - "Microsoft.Data.SqlClient", - "Microsoft.EntityFrameworkCore.Design", - "Microsoft.EntityFrameworkCore.InMemory", - "Microsoft.EntityFrameworkCore.Relational", - "Microsoft.EntityFrameworkCore.Sqlite", - "Microsoft.EntityFrameworkCore.SqlServer", - "Microsoft.Extensions.Caching.Cosmos", - "Microsoft.Extensions.Caching.SqlServer", - "Microsoft.Extensions.Caching.StackExchangeRedis", - "Npgsql.EntityFrameworkCore.PostgreSQL", - "Pomelo.EntityFrameworkCore.MySql" - ], - "description": "DbOps owned dependencies", - "commitMessagePrefix": "[deps] DbOps:", - "reviewers": ["team:dept-dbops"] - }, - { - "matchPackageNames": ["CommandDotNet", "YamlDotNet"], - "description": "DevOps owned dependencies", - "commitMessagePrefix": "[deps] BRE:", - "reviewers": ["team:dept-bre"] - }, - { - "matchPackageNames": [ - "AspNetCoreRateLimit", - "AspNetCoreRateLimit.Redis", - "Azure.Data.Tables", - "Azure.Messaging.EventGrid", - "Azure.Messaging.ServiceBus", - "Azure.Storage.Blobs", - "Azure.Storage.Queues", - "Microsoft.AspNetCore.Authentication.JwtBearer", - "Microsoft.AspNetCore.Http", - "Quartz" - ], - "description": "Platform owned dependencies", - "commitMessagePrefix": "[deps] Platform:", - "reviewers": ["team:team-platform-dev"] - }, - { - "matchPackagePatterns": ["EntityFrameworkCore", "^dotnet-ef"], - "groupName": "EntityFrameworkCore", - "description": "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset" - }, - { - "matchPackageNames": [ - "AutoMapper.Extensions.Microsoft.DependencyInjection", - "AWSSDK.SimpleEmail", - "AWSSDK.SQS", - "Handlebars.Net", - "LaunchDarkly.ServerSdk", - "MailKit", - "Microsoft.AspNetCore.SignalR.Protocols.MessagePack", - "Microsoft.AspNetCore.SignalR.StackExchangeRedis", - "Microsoft.Azure.NotificationHubs", - "Microsoft.Extensions.Configuration.EnvironmentVariables", - "Microsoft.Extensions.Configuration.UserSecrets", - "Microsoft.Extensions.Configuration", - "Microsoft.Extensions.DependencyInjection.Abstractions", - "Microsoft.Extensions.DependencyInjection", - "SendGrid" - ], - "description": "Tools owned dependencies", - "commitMessagePrefix": "[deps] Tools:", - "reviewers": ["team:team-tools-dev"] - }, - { - "matchPackagePatterns": ["^Microsoft.AspNetCore.SignalR"], - "groupName": "SignalR", - "description": "Group SignalR to exclude them from the dotnet monorepo preset" - }, - { - "matchPackagePatterns": ["^Microsoft.Extensions.Configuration"], - "groupName": "Microsoft.Extensions.Configuration", - "description": "Group Microsoft.Extensions.Configuration to exclude them from the dotnet monorepo preset" - }, - { - "matchPackagePatterns": ["^Microsoft.Extensions.DependencyInjection"], - "groupName": "Microsoft.Extensions.DependencyInjection", - "description": "Group Microsoft.Extensions.DependencyInjection to exclude them from the dotnet monorepo preset" - }, - { - "matchPackageNames": [ - "AngleSharp", - "AspNetCore.HealthChecks.AzureServiceBus", - "AspNetCore.HealthChecks.AzureStorage", - "AspNetCore.HealthChecks.Network", - "AspNetCore.HealthChecks.Redis", - "AspNetCore.HealthChecks.SendGrid", - "AspNetCore.HealthChecks.SqlServer", - "AspNetCore.HealthChecks.Uris" - ], - "description": "Vault owned dependencies", - "commitMessagePrefix": "[deps] Vault:", - "reviewers": ["team:team-vault-dev"] - } - ], - "ignoreDeps": ["dotnet-sdk"] -} diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 0000000000..4722307d10 --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,199 @@ +{ + $schema: "https://docs.renovatebot.com/renovate-schema.json", + extends: ["github>bitwarden/renovate-config"], // Extends our default configuration for pinned dependencies + enabledManagers: [ + "dockerfile", + "docker-compose", + "github-actions", + "npm", + "nuget", + ], + packageRules: [ + { + groupName: "dockerfile minor", + matchManagers: ["dockerfile"], + matchUpdateTypes: ["minor"], + }, + { + groupName: "docker-compose minor", + matchManagers: ["docker-compose"], + matchUpdateTypes: ["minor"], + }, + { + groupName: "github-action minor", + matchManagers: ["github-actions"], + matchUpdateTypes: ["minor"], + }, + { + matchManagers: ["dockerfile", "docker-compose"], + commitMessagePrefix: "[deps] BRE:", + }, + { + matchPackageNames: ["DnsClient"], + description: "Admin Console owned dependencies", + commitMessagePrefix: "[deps] AC:", + reviewers: ["team:team-admin-console-dev"], + }, + { + matchFileNames: ["src/Admin/package.json", "src/Sso/package.json"], + description: "Admin & SSO npm packages", + commitMessagePrefix: "[deps] Auth:", + reviewers: ["team:team-auth-dev"], + }, + { + matchPackageNames: [ + "Azure.Extensions.AspNetCore.DataProtection.Blobs", + "DuoUniversal", + "Fido2.AspNet", + "Duende.IdentityServer", + "Microsoft.Extensions.Identity.Stores", + "Otp.NET", + "Sustainsys.Saml2.AspNetCore2", + "YubicoDotNetClient", + ], + description: "Auth owned dependencies", + commitMessagePrefix: "[deps] Auth:", + reviewers: ["team:team-auth-dev"], + }, + { + matchPackageNames: [ + "AutoFixture.AutoNSubstitute", + "AutoFixture.Xunit2", + "BenchmarkDotNet", + "BitPay.Light", + "Braintree", + "coverlet.collector", + "CsvHelper", + "Kralizek.AutoFixture.Extensions.MockHttp", + "Microsoft.AspNetCore.Mvc.Testing", + "Microsoft.Extensions.Logging", + "Microsoft.Extensions.Logging.Console", + "Newtonsoft.Json", + "NSubstitute", + "Sentry.Serilog", + "Serilog.AspNetCore", + "Serilog.Extensions.Logging", + "Serilog.Extensions.Logging.File", + "Serilog.Sinks.AzureCosmosDB", + "Serilog.Sinks.SyslogMessages", + "Stripe.net", + "Swashbuckle.AspNetCore", + "Swashbuckle.AspNetCore.SwaggerGen", + "xunit", + "xunit.runner.visualstudio", + ], + description: "Billing owned dependencies", + commitMessagePrefix: "[deps] Billing:", + reviewers: ["team:team-billing-dev"], + }, + { + matchPackagePatterns: ["^Microsoft.Extensions.Logging"], + groupName: "Microsoft.Extensions.Logging", + description: "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset", + }, + { + matchPackageNames: [ + "Dapper", + "dbup-sqlserver", + "dotnet-ef", + "linq2db.EntityFrameworkCore", + "Microsoft.Azure.Cosmos", + "Microsoft.Data.SqlClient", + "Microsoft.EntityFrameworkCore.Design", + "Microsoft.EntityFrameworkCore.InMemory", + "Microsoft.EntityFrameworkCore.Relational", + "Microsoft.EntityFrameworkCore.Sqlite", + "Microsoft.EntityFrameworkCore.SqlServer", + "Microsoft.Extensions.Caching.Cosmos", + "Microsoft.Extensions.Caching.SqlServer", + "Microsoft.Extensions.Caching.StackExchangeRedis", + "Npgsql.EntityFrameworkCore.PostgreSQL", + "Pomelo.EntityFrameworkCore.MySql", + ], + description: "DbOps owned dependencies", + commitMessagePrefix: "[deps] DbOps:", + reviewers: ["team:dept-dbops"], + }, + { + matchPackageNames: ["CommandDotNet", "YamlDotNet"], + description: "DevOps owned dependencies", + commitMessagePrefix: "[deps] BRE:", + reviewers: ["team:dept-bre"], + }, + { + matchPackageNames: [ + "AspNetCoreRateLimit", + "AspNetCoreRateLimit.Redis", + "Azure.Data.Tables", + "Azure.Messaging.EventGrid", + "Azure.Messaging.ServiceBus", + "Azure.Storage.Blobs", + "Azure.Storage.Queues", + "Microsoft.AspNetCore.Authentication.JwtBearer", + "Microsoft.AspNetCore.Http", + "Quartz", + ], + description: "Platform owned dependencies", + commitMessagePrefix: "[deps] Platform:", + reviewers: ["team:team-platform-dev"], + }, + { + matchPackagePatterns: ["EntityFrameworkCore", "^dotnet-ef"], + groupName: "EntityFrameworkCore", + description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset", + }, + { + matchPackageNames: [ + "AutoMapper.Extensions.Microsoft.DependencyInjection", + "AWSSDK.SimpleEmail", + "AWSSDK.SQS", + "Handlebars.Net", + "LaunchDarkly.ServerSdk", + "MailKit", + "Microsoft.AspNetCore.SignalR.Protocols.MessagePack", + "Microsoft.AspNetCore.SignalR.StackExchangeRedis", + "Microsoft.Azure.NotificationHubs", + "Microsoft.Extensions.Configuration.EnvironmentVariables", + "Microsoft.Extensions.Configuration.UserSecrets", + "Microsoft.Extensions.Configuration", + "Microsoft.Extensions.DependencyInjection.Abstractions", + "Microsoft.Extensions.DependencyInjection", + "SendGrid", + ], + description: "Tools owned dependencies", + commitMessagePrefix: "[deps] Tools:", + reviewers: ["team:team-tools-dev"], + }, + { + matchPackagePatterns: ["^Microsoft.AspNetCore.SignalR"], + groupName: "SignalR", + description: "Group SignalR to exclude them from the dotnet monorepo preset", + }, + { + matchPackagePatterns: ["^Microsoft.Extensions.Configuration"], + groupName: "Microsoft.Extensions.Configuration", + description: "Group Microsoft.Extensions.Configuration to exclude them from the dotnet monorepo preset", + }, + { + matchPackagePatterns: ["^Microsoft.Extensions.DependencyInjection"], + groupName: "Microsoft.Extensions.DependencyInjection", + description: "Group Microsoft.Extensions.DependencyInjection to exclude them from the dotnet monorepo preset", + }, + { + matchPackageNames: [ + "AngleSharp", + "AspNetCore.HealthChecks.AzureServiceBus", + "AspNetCore.HealthChecks.AzureStorage", + "AspNetCore.HealthChecks.Network", + "AspNetCore.HealthChecks.Redis", + "AspNetCore.HealthChecks.SendGrid", + "AspNetCore.HealthChecks.SqlServer", + "AspNetCore.HealthChecks.Uris", + ], + description: "Vault owned dependencies", + commitMessagePrefix: "[deps] Vault:", + reviewers: ["team:team-vault-dev"], + }, + ], + ignoreDeps: ["dotnet-sdk"], +} From fcb98481800fc410c2d445be04ebee350f9e37a3 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:13:48 +0100 Subject: [PATCH 145/148] [PM-13620]Existing user email linking to create-organization (#5315) * Changes for the existing customer Signed-off-by: Cy Okeke * removed the added character Signed-off-by: Cy Okeke --------- Signed-off-by: Cy Okeke --- .../Models/Mail/TrialInititaionVerifyEmail.cs | 19 +++++++++++++++---- ...alInitiationEmailForRegistrationCommand.cs | 5 +---- src/Core/Services/IMailService.cs | 1 + .../Implementations/HandlebarsMailService.cs | 2 ++ .../NoopImplementations/NoopMailService.cs | 1 + 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs b/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs index df08296083..33b9578d0e 100644 --- a/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs +++ b/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs @@ -5,6 +5,7 @@ namespace Bit.Core.Billing.Models.Mail; public class TrialInitiationVerifyEmail : RegisterVerifyEmail { + public bool IsExistingUser { get; set; } /// /// See comment on . /// @@ -26,8 +27,18 @@ public class TrialInitiationVerifyEmail : RegisterVerifyEmail /// Currently we only support one product type at a time, despite Product being a collection. /// If we receive both PasswordManager and SecretsManager, we'll send the user to the PM trial route ///
- private string Route => - Product.Any(p => p == ProductType.PasswordManager) - ? "trial-initiation" - : "secrets-manager-trial-initiation"; + private string Route + { + get + { + if (IsExistingUser) + { + return "create-organization"; + } + + return Product.Any(p => p == ProductType.PasswordManager) + ? "trial-initiation" + : "secrets-manager-trial-initiation"; + } + } } diff --git a/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs b/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs index 6657be085e..385d7ebbd6 100644 --- a/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs +++ b/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs @@ -43,10 +43,7 @@ public class SendTrialInitiationEmailForRegistrationCommand( await PerformConstantTimeOperationsAsync(); - if (!userExists) - { - await mailService.SendTrialInitiationSignupEmailAsync(email, token, productTier, products); - } + await mailService.SendTrialInitiationSignupEmailAsync(userExists, email, token, productTier, products); return null; } diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 77914c0188..92d05ddb7d 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -14,6 +14,7 @@ public interface IMailService Task SendVerifyEmailEmailAsync(string email, Guid userId, string token); Task SendRegistrationVerificationEmailAsync(string email, string token); Task SendTrialInitiationSignupEmailAsync( + bool isExistingUser, string email, string token, ProductTierType productTier, diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 630c5b0bf0..d18a29b13a 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -74,6 +74,7 @@ public class HandlebarsMailService : IMailService } public async Task SendTrialInitiationSignupEmailAsync( + bool isExistingUser, string email, string token, ProductTierType productTier, @@ -82,6 +83,7 @@ public class HandlebarsMailService : IMailService var message = CreateDefaultMessage("Verify your email", email); var model = new TrialInitiationVerifyEmail { + IsExistingUser = isExistingUser, Token = WebUtility.UrlEncode(token), Email = WebUtility.UrlEncode(email), WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 13914ddd86..d6b330294d 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -26,6 +26,7 @@ public class NoopMailService : IMailService } public Task SendTrialInitiationSignupEmailAsync( + bool isExistingUser, string email, string token, ProductTierType productTier, From 43be2dbc8304e1fdc36aa58f14e63471e9bdada0 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 19 Feb 2025 09:52:17 -0500 Subject: [PATCH 146/148] Prevent organization disablement on addition to provider (#5419) --- .../Services/Implementations/SubscriptionDeletedHandler.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs b/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs index 06692ab016..26a1c30c14 100644 --- a/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs @@ -33,13 +33,16 @@ public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler var subCanceled = subscription.Status == StripeSubscriptionStatus.Canceled; const string providerMigrationCancellationComment = "Cancelled as part of provider migration to Consolidated Billing"; + const string addedToProviderCancellationComment = "Organization was added to Provider"; if (!subCanceled) { return; } - if (organizationId.HasValue && subscription is not { CancellationDetails.Comment: providerMigrationCancellationComment }) + if (organizationId.HasValue && + subscription.CancellationDetails.Comment != providerMigrationCancellationComment && + !subscription.CancellationDetails.Comment.Contains(addedToProviderCancellationComment)) { await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); } From 4f73081e4158bafad74dbd9e2142ec423885a9bf Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 19 Feb 2025 10:13:03 -0500 Subject: [PATCH 147/148] Give provider credit for unused client organization time (#5421) --- .../Billing/ProviderBillingService.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index abba8aff90..da59c3c35c 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -117,6 +117,19 @@ public class ProviderBillingService( ScaleSeats(provider, organization.PlanType, organization.Seats!.Value) ); + var clientCustomer = await subscriberService.GetCustomer(organization); + + if (clientCustomer.Balance != 0) + { + await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId, + new CustomerBalanceTransactionCreateOptions + { + Amount = clientCustomer.Balance, + Currency = "USD", + Description = $"Unused, prorated time for client organization with ID {organization.Id}." + }); + } + await eventService.LogProviderOrganizationEventAsync( providerOrganization, EventType.ProviderOrganization_Added); From 228ce3b2e9d9b58fae89532d5858c1e0ec1139d2 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 19 Feb 2025 12:01:11 -0500 Subject: [PATCH 148/148] Scale seats before inserting ProviderOrganization when adding existing organization (#5420) --- .../Commercial.Core/Billing/ProviderBillingService.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index da59c3c35c..7b10793283 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -111,10 +111,15 @@ public class ProviderBillingService( Key = key }; + /* + * We have to scale the provider's seats before the ProviderOrganization + * row is inserted so the added organization's seats don't get double counted. + */ + await ScaleSeats(provider, organization.PlanType, organization.Seats!.Value); + await Task.WhenAll( organizationRepository.ReplaceAsync(organization), - providerOrganizationRepository.CreateAsync(providerOrganization), - ScaleSeats(provider, organization.PlanType, organization.Seats!.Value) + providerOrganizationRepository.CreateAsync(providerOrganization) ); var clientCustomer = await subscriberService.GetCustomer(organization);