From 25afd50ab4f804a01e05024fd82aebcd2428fdc9 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Tue, 12 Nov 2024 14:53:34 +0100 Subject: [PATCH 01/16] [PM-14798] Update ProviderEventService for multi-organization enterprises (#5026) --- .../Implementations/ProviderEventService.cs | 85 +++++++------------ .../Services/ProviderEventServiceTests.cs | 49 ----------- 2 files changed, 29 insertions(+), 105 deletions(-) diff --git a/src/Billing/Services/Implementations/ProviderEventService.cs b/src/Billing/Services/Implementations/ProviderEventService.cs index 7ce72526ccb1..548ed9f54768 100644 --- a/src/Billing/Services/Implementations/ProviderEventService.cs +++ b/src/Billing/Services/Implementations/ProviderEventService.cs @@ -1,7 +1,6 @@ using Bit.Billing.Constants; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Enums; using Bit.Core.Billing.Repositories; using Bit.Core.Enums; using Bit.Core.Utilities; @@ -42,78 +41,52 @@ public async Task TryRecordInvoiceLineItems(Event parsedEvent) case HandledStripeWebhook.InvoiceCreated: { var clients = - (await providerOrganizationRepository.GetManyDetailsByProviderAsync(parsedProviderId)) - .Where(providerOrganization => providerOrganization.Status == OrganizationStatusType.Managed); + await providerOrganizationRepository.GetManyDetailsByProviderAsync(parsedProviderId); var providerPlans = await providerPlanRepository.GetByProviderId(parsedProviderId); - var enterpriseProviderPlan = - providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly); + var invoiceItems = new List(); - var teamsProviderPlan = - providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly); - - if (enterpriseProviderPlan == null || !enterpriseProviderPlan.IsConfigured() || - teamsProviderPlan == null || !teamsProviderPlan.IsConfigured()) + foreach (var client in clients) { - logger.LogError("Provider {ProviderID} is missing or has misconfigured provider plans", parsedProviderId); - - throw new Exception("Cannot record invoice line items for Provider with missing or misconfigured provider plans"); - } - - var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly); - - var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - - var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100; - - var discountedEnterpriseSeatPrice = enterprisePlan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage; + if (client.Status != OrganizationStatusType.Managed) + { + continue; + } - var discountedTeamsSeatPrice = teamsPlan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage; + var plan = StaticStore.Plans.Single(x => x.Name == client.Plan && providerPlans.Any(y => y.PlanType == x.Type)); - var invoiceItems = clients.Select(client => new ProviderInvoiceItem - { - ProviderId = parsedProviderId, - InvoiceId = invoice.Id, - InvoiceNumber = invoice.Number, - ClientId = client.OrganizationId, - ClientName = client.OrganizationName, - PlanName = client.Plan, - AssignedSeats = client.Seats ?? 0, - UsedSeats = client.OccupiedSeats ?? 0, - Total = client.Plan == enterprisePlan.Name - ? (client.Seats ?? 0) * discountedEnterpriseSeatPrice - : (client.Seats ?? 0) * discountedTeamsSeatPrice - }).ToList(); - - if (enterpriseProviderPlan.PurchasedSeats is null or 0) - { - var enterpriseClientSeats = invoiceItems - .Where(item => item.PlanName == enterprisePlan.Name) - .Sum(item => item.AssignedSeats); + var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100; - var unassignedEnterpriseSeats = enterpriseProviderPlan.SeatMinimum - enterpriseClientSeats ?? 0; + var discountedSeatPrice = plan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage; invoiceItems.Add(new ProviderInvoiceItem { ProviderId = parsedProviderId, InvoiceId = invoice.Id, InvoiceNumber = invoice.Number, - ClientName = "Unassigned seats", - PlanName = enterprisePlan.Name, - AssignedSeats = unassignedEnterpriseSeats, - UsedSeats = 0, - Total = unassignedEnterpriseSeats * discountedEnterpriseSeatPrice + ClientId = client.OrganizationId, + ClientName = client.OrganizationName, + PlanName = client.Plan, + AssignedSeats = client.Seats ?? 0, + UsedSeats = client.OccupiedSeats ?? 0, + Total = (client.Seats ?? 0) * discountedSeatPrice }); } - if (teamsProviderPlan.PurchasedSeats is null or 0) + foreach (var providerPlan in providerPlans.Where(x => x.PurchasedSeats is null or 0)) { - var teamsClientSeats = invoiceItems - .Where(item => item.PlanName == teamsPlan.Name) + var plan = StaticStore.GetPlan(providerPlan.PlanType); + + var clientSeats = invoiceItems + .Where(item => item.PlanName == plan.Name) .Sum(item => item.AssignedSeats); - var unassignedTeamsSeats = teamsProviderPlan.SeatMinimum - teamsClientSeats ?? 0; + var unassignedSeats = providerPlan.SeatMinimum - clientSeats ?? 0; + + var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100; + + var discountedSeatPrice = plan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage; invoiceItems.Add(new ProviderInvoiceItem { @@ -121,10 +94,10 @@ public async Task TryRecordInvoiceLineItems(Event parsedEvent) InvoiceId = invoice.Id, InvoiceNumber = invoice.Number, ClientName = "Unassigned seats", - PlanName = teamsPlan.Name, - AssignedSeats = unassignedTeamsSeats, + PlanName = plan.Name, + AssignedSeats = unassignedSeats, UsedSeats = 0, - Total = unassignedTeamsSeats * discountedTeamsSeatPrice + Total = unassignedSeats * discountedSeatPrice }); } diff --git a/test/Billing.Test/Services/ProviderEventServiceTests.cs b/test/Billing.Test/Services/ProviderEventServiceTests.cs index e76cf0d284fc..31f4ec8969b0 100644 --- a/test/Billing.Test/Services/ProviderEventServiceTests.cs +++ b/test/Billing.Test/Services/ProviderEventServiceTests.cs @@ -8,7 +8,6 @@ using Bit.Core.Billing.Repositories; using Bit.Core.Enums; using Bit.Core.Utilities; -using FluentAssertions; using Microsoft.Extensions.Logging; using NSubstitute; using Stripe; @@ -89,54 +88,6 @@ public async Task TryRecordInvoiceLineItems_EventNotProviderRelated_NoOp() await _providerOrganizationRepository.DidNotReceiveWithAnyArgs().GetManyDetailsByProviderAsync(Arg.Any()); } - [Fact] - public async Task TryRecordInvoiceLineItems_InvoiceCreated_MisconfiguredProviderPlans_ThrowsException() - { - // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated); - - const string subscriptionId = "sub_1"; - var providerId = Guid.NewGuid(); - - var invoice = new Invoice - { - SubscriptionId = subscriptionId - }; - - _stripeEventService.GetInvoice(stripeEvent).Returns(invoice); - - var subscription = new Subscription - { - Metadata = new Dictionary { { "providerId", providerId.ToString() } } - }; - - _stripeFacade.GetSubscription(subscriptionId).Returns(subscription); - - var providerPlans = new List - { - new () - { - Id = Guid.NewGuid(), - ProviderId = providerId, - PlanType = PlanType.TeamsMonthly, - AllocatedSeats = 0, - PurchasedSeats = 0, - SeatMinimum = 100 - } - }; - - _providerPlanRepository.GetByProviderId(providerId).Returns(providerPlans); - - // Act - var function = async () => await _providerEventService.TryRecordInvoiceLineItems(stripeEvent); - - // Assert - await function - .Should() - .ThrowAsync() - .WithMessage("Cannot record invoice line items for Provider with missing or misconfigured provider plans"); - } - [Fact] public async Task TryRecordInvoiceLineItems_InvoiceCreated_Succeeds() { From f2bf9ea9f8bdea0a70741fe26a09ec432013f66f Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Tue, 12 Nov 2024 11:25:36 -0600 Subject: [PATCH 02/16] [PM-12479] - Adding group-details endpoint (#4959) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ Added group-details endpoint. Moved group auth handler to AdminConsole directory. --------- Co-authored-by: Matt Bishop --- .../Controllers/GroupsController.cs | 36 ++- .../OrganizationUsersController.cs | 1 + src/Api/Api.csproj | 1 - .../Utilities/ServiceCollectionExtensions.cs | 2 +- .../Groups/GroupAuthorizationHandler.cs | 62 ----- .../Groups/GroupOperations.cs | 22 -- .../GroupAuthorizationHandler.cs | 43 ++++ .../Groups/Authorization/GroupOperations.cs | 17 ++ ...tionUserUserDetailsAuthorizationHandler.cs | 1 + ...UserUserMiniDetailsAuthorizationHandler.cs | 10 +- .../Authorization/OrganizationScope.cs | 2 +- src/Core/Constants.cs | 1 + .../GroupAuthorizationHandlerTests.cs | 223 ++++++++++++++++++ .../GroupAuthorizationHandlerTests.cs | 125 ---------- ...serUserDetailsAuthorizationHandlerTests.cs | 1 + ...serMiniDetailsAuthorizationHandlerTests.cs | 1 + 16 files changed, 323 insertions(+), 225 deletions(-) delete mode 100644 src/Api/Vault/AuthorizationHandlers/Groups/GroupAuthorizationHandler.cs delete mode 100644 src/Api/Vault/AuthorizationHandlers/Groups/GroupOperations.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupAuthorizationHandler.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupOperations.cs rename src/Core/AdminConsole/OrganizationFeatures/{OrganizationUsers => Shared}/Authorization/OrganizationScope.cs (90%) create mode 100644 test/Api.Test/AdminConsole/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs delete mode 100644 test/Api.Test/Vault/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs diff --git a/src/Api/AdminConsole/Controllers/GroupsController.cs b/src/Api/AdminConsole/Controllers/GroupsController.cs index f3f1d343c275..0e4665699459 100644 --- a/src/Api/AdminConsole/Controllers/GroupsController.cs +++ b/src/Api/AdminConsole/Controllers/GroupsController.cs @@ -2,15 +2,16 @@ using Bit.Api.AdminConsole.Models.Response; using Bit.Api.Models.Response; using Bit.Api.Vault.AuthorizationHandlers.Collections; -using Bit.Api.Vault.AuthorizationHandlers.Groups; +using Bit.Core; +using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -89,11 +90,34 @@ public async Task GetDetails(string orgId, string id) } [HttpGet("")] - public async Task> Get(Guid orgId) + public async Task> GetOrganizationGroups(Guid orgId) { - var authorized = - (await _authorizationService.AuthorizeAsync(User, GroupOperations.ReadAll(orgId))).Succeeded; - if (!authorized) + var authResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAll); + if (!authResult.Succeeded) + { + throw new NotFoundException(); + } + + if (_featureService.IsEnabled(FeatureFlagKeys.SecureOrgGroupDetails)) + { + var groups = await _groupRepository.GetManyByOrganizationIdAsync(orgId); + var responses = groups.Select(g => new GroupDetailsResponseModel(g, [])); + return new ListResponseModel(responses); + } + + var groupDetails = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(orgId); + var detailResponses = groupDetails.Select(g => new GroupDetailsResponseModel(g.Item1, g.Item2)); + return new ListResponseModel(detailResponses); + } + + [HttpGet("details")] + public async Task> GetOrganizationGroupDetails(Guid orgId) + { + var authResult = _featureService.IsEnabled(FeatureFlagKeys.SecureOrgGroupDetails) + ? await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAllDetails) + : await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAll); + + if (!authResult.Succeeded) { throw new NotFoundException(); } diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index b81cb068fade..3193962fa949 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index d6d055e90e7d..4a018b2198a0 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -1,5 +1,4 @@  - bitwarden-Api false diff --git a/src/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index a7fbddbaa486..8a58a5f2364a 100644 --- a/src/Api/Utilities/ServiceCollectionExtensions.cs +++ b/src/Api/Utilities/ServiceCollectionExtensions.cs @@ -1,5 +1,5 @@ using Bit.Api.Vault.AuthorizationHandlers.Collections; -using Bit.Api.Vault.AuthorizationHandlers.Groups; +using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; using Bit.Core.IdentityServer; using Bit.Core.Settings; using Bit.Core.Utilities; diff --git a/src/Api/Vault/AuthorizationHandlers/Groups/GroupAuthorizationHandler.cs b/src/Api/Vault/AuthorizationHandlers/Groups/GroupAuthorizationHandler.cs deleted file mode 100644 index 666cd725e4fd..000000000000 --- a/src/Api/Vault/AuthorizationHandlers/Groups/GroupAuthorizationHandler.cs +++ /dev/null @@ -1,62 +0,0 @@ -#nullable enable -using Bit.Core.Context; -using Microsoft.AspNetCore.Authorization; - -namespace Bit.Api.Vault.AuthorizationHandlers.Groups; - -/// -/// Handles authorization logic for Group operations. -/// This uses new logic implemented in the Flexible Collections initiative. -/// -public class GroupAuthorizationHandler : AuthorizationHandler -{ - private readonly ICurrentContext _currentContext; - - public GroupAuthorizationHandler(ICurrentContext currentContext) - { - _currentContext = currentContext; - } - - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, - GroupOperationRequirement requirement) - { - // Acting user is not authenticated, fail - if (!_currentContext.UserId.HasValue) - { - context.Fail(); - return; - } - - if (requirement.OrganizationId == default) - { - context.Fail(); - return; - } - - var org = _currentContext.GetOrganization(requirement.OrganizationId); - - switch (requirement) - { - case not null when requirement.Name == nameof(GroupOperations.ReadAll): - await CanReadAllAsync(context, requirement, org); - break; - } - } - - private async Task CanReadAllAsync(AuthorizationHandlerContext context, GroupOperationRequirement requirement, - CurrentContextOrganization? org) - { - // All users of an organization can read all groups belonging to the organization for collection access management - if (org is not null) - { - context.Succeed(requirement); - return; - } - - // Allow provider users to read all groups if they are a provider for the target organization - if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId)) - { - context.Succeed(requirement); - } - } -} diff --git a/src/Api/Vault/AuthorizationHandlers/Groups/GroupOperations.cs b/src/Api/Vault/AuthorizationHandlers/Groups/GroupOperations.cs deleted file mode 100644 index 7735bd52a1bd..000000000000 --- a/src/Api/Vault/AuthorizationHandlers/Groups/GroupOperations.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.AspNetCore.Authorization.Infrastructure; - -namespace Bit.Api.Vault.AuthorizationHandlers.Groups; - -public class GroupOperationRequirement : OperationAuthorizationRequirement -{ - public Guid OrganizationId { get; init; } - - public GroupOperationRequirement(string name, Guid organizationId) - { - Name = name; - OrganizationId = organizationId; - } -} - -public static class GroupOperations -{ - public static GroupOperationRequirement ReadAll(Guid organizationId) - { - return new GroupOperationRequirement(nameof(ReadAll), organizationId); - } -} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupAuthorizationHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupAuthorizationHandler.cs new file mode 100644 index 000000000000..d531c1175d38 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupAuthorizationHandler.cs @@ -0,0 +1,43 @@ +#nullable enable +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +using Bit.Core.Context; +using Bit.Core.Enums; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; + +public class GroupAuthorizationHandler(ICurrentContext currentContext) + : AuthorizationHandler +{ + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + GroupOperationRequirement requirement, OrganizationScope organizationScope) + { + var authorized = requirement switch + { + not null when requirement.Name == nameof(GroupOperations.ReadAll) => + await CanReadAllAsync(organizationScope), + not null when requirement.Name == nameof(GroupOperations.ReadAllDetails) => + await CanViewGroupDetailsAsync(organizationScope), + _ => false + }; + + if (requirement is not null && authorized) + { + context.Succeed(requirement); + } + } + + private async Task CanReadAllAsync(OrganizationScope organizationScope) => + currentContext.GetOrganization(organizationScope) is not null + || await currentContext.ProviderUserForOrgAsync(organizationScope); + + private async Task CanViewGroupDetailsAsync(OrganizationScope organizationScope) => + currentContext.GetOrganization(organizationScope) is + { Type: OrganizationUserType.Owner } or + { Type: OrganizationUserType.Admin } or + { + Permissions: { ManageGroups: true } or + { ManageUsers: true } + } || + await currentContext.ProviderUserForOrgAsync(organizationScope); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupOperations.cs b/src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupOperations.cs new file mode 100644 index 000000000000..f5b3b975f9e9 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupOperations.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; + +public class GroupOperationRequirement : OperationAuthorizationRequirement +{ + public GroupOperationRequirement(string name) + { + Name = name; + } +} + +public static class GroupOperations +{ + public static readonly GroupOperationRequirement ReadAll = new(nameof(ReadAll)); + public static readonly GroupOperationRequirement ReadAllDetails = new(nameof(ReadAllDetails)); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserDetailsAuthorizationHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserDetailsAuthorizationHandler.cs index e890e4d9ffbc..f57850e640db 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserDetailsAuthorizationHandler.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserDetailsAuthorizationHandler.cs @@ -1,4 +1,5 @@ #nullable enable +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.Context; using Bit.Core.Enums; using Microsoft.AspNetCore.Authorization; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandler.cs index 24d9d0c260bf..01b77a05b3e6 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandler.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandler.cs @@ -1,5 +1,5 @@ -using Bit.Core.Context; -using Bit.Core.Services; +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +using Bit.Core.Context; using Microsoft.AspNetCore.Authorization; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; @@ -7,14 +7,10 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authoriza public class OrganizationUserUserMiniDetailsAuthorizationHandler : AuthorizationHandler { - private readonly IApplicationCacheService _applicationCacheService; private readonly ICurrentContext _currentContext; - public OrganizationUserUserMiniDetailsAuthorizationHandler( - IApplicationCacheService applicationCacheService, - ICurrentContext currentContext) + public OrganizationUserUserMiniDetailsAuthorizationHandler(ICurrentContext currentContext) { - _applicationCacheService = applicationCacheService; _currentContext = currentContext; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationScope.cs b/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationScope.cs similarity index 90% rename from src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationScope.cs rename to src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationScope.cs index 57856e702014..689cff24efa7 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationScope.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationScope.cs @@ -1,6 +1,6 @@ #nullable enable -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; +namespace Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; /// /// A typed wrapper for an organization Guid. This is used for authorization checks diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index f7592074976a..67d463bac9c5 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -143,6 +143,7 @@ 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 SecureOrgGroupDetails = "pm-3479-secure-org-group-details"; public const string AccessIntelligence = "pm-13227-access-intelligence"; public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises"; diff --git a/test/Api.Test/AdminConsole/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs b/test/Api.Test/AdminConsole/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs new file mode 100644 index 000000000000..a1b63c4fd047 --- /dev/null +++ b/test/Api.Test/AdminConsole/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs @@ -0,0 +1,223 @@ +using System.Security.Claims; +using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.AdminConsole.AuthorizationHandlers; + +[SutProviderCustomize] +public class GroupAuthorizationHandlerTests +{ + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task CanReadAllAsync_WhenMemberOfOrg_Success( + OrganizationUserType userType, + OrganizationScope scope, + Guid userId, SutProvider sutProvider, + CurrentContextOrganization organization) + { + organization.Type = userType; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + new[] { GroupOperations.ReadAll }, + new ClaimsPrincipal(), + scope); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(scope).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task CanReadAllAsync_WithProviderUser_Success( + Guid userId, + OrganizationScope scope, + SutProvider sutProvider, CurrentContextOrganization organization) + { + organization.Type = OrganizationUserType.User; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + new[] { GroupOperations.ReadAll }, + new ClaimsPrincipal(), + scope); + + sutProvider.GetDependency() + .UserId + .Returns(userId); + sutProvider.GetDependency() + .ProviderUserForOrgAsync(scope) + .Returns(true); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task CanReadAllAsync_WhenMissingOrgAccess_NoSuccess( + Guid userId, + OrganizationScope scope, + SutProvider sutProvider) + { + + var context = new AuthorizationHandlerContext( + new[] { GroupOperations.ReadAll }, + new ClaimsPrincipal(), + scope + ); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); + sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); + + await sutProvider.Sut.HandleAsync(context); + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenUserIsAdminOwner_ThenShouldSucceed(OrganizationUserType userType, + OrganizationScope scope, + CurrentContextOrganization organization, SutProvider sutProvider) + { + organization.Type = userType; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + [GroupOperations.ReadAllDetails], + new ClaimsPrincipal(), + scope + ); + + sutProvider.GetDependency().GetOrganization(scope).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenUserIsNotOwnerOrAdmin_ThenShouldFail(OrganizationUserType userType, + OrganizationScope scope, + CurrentContextOrganization organization, SutProvider sutProvider) + { + organization.Type = userType; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + [GroupOperations.ReadAllDetails], + new ClaimsPrincipal(), + scope + ); + + sutProvider.GetDependency().GetOrganization(scope).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenUserHasManageGroupPermission_ThenShouldSucceed( + OrganizationScope scope, + CurrentContextOrganization organization, SutProvider sutProvider) + { + organization.Type = OrganizationUserType.Custom; + organization.Permissions = new Permissions + { + ManageGroups = true + }; + + var context = new AuthorizationHandlerContext( + [GroupOperations.ReadAllDetails], + new ClaimsPrincipal(), + scope + ); + + sutProvider.GetDependency().GetOrganization(scope).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenUserHasManageUserPermission_ThenShouldSucceed( + OrganizationScope scope, + CurrentContextOrganization organization, SutProvider sutProvider) + { + organization.Type = OrganizationUserType.Custom; + organization.Permissions = new Permissions + { + ManageUsers = true + }; + + var context = new AuthorizationHandlerContext( + [GroupOperations.ReadAllDetails], + new ClaimsPrincipal(), + scope + ); + + sutProvider.GetDependency().GetOrganization(scope).Returns(organization); + + await sutProvider.Sut.HandleAsync(context); + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenUserIsStandardUserTypeWithoutElevatedPermissions_ThenShouldFail( + OrganizationScope scope, + CurrentContextOrganization organization, SutProvider sutProvider) + { + organization.Type = OrganizationUserType.User; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + [GroupOperations.ReadAllDetails], + new ClaimsPrincipal(), + scope + ); + + sutProvider.GetDependency().GetOrganization(scope).Returns(organization); + sutProvider.GetDependency().ProviderUserForOrgAsync(scope).Returns(false); + + await sutProvider.Sut.HandleAsync(context); + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenIsProviderUser_ThenShouldSucceed( + OrganizationScope scope, + SutProvider sutProvider, CurrentContextOrganization organization) + { + organization.Type = OrganizationUserType.User; + organization.Permissions = new Permissions(); + + var context = new AuthorizationHandlerContext( + new[] { GroupOperations.ReadAll }, + new ClaimsPrincipal(), + scope); + + sutProvider.GetDependency().GetOrganization(scope).Returns(organization); + sutProvider.GetDependency().ProviderUserForOrgAsync(scope).Returns(true); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } +} diff --git a/test/Api.Test/Vault/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs b/test/Api.Test/Vault/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs deleted file mode 100644 index 608e201c50ab..000000000000 --- a/test/Api.Test/Vault/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System.Security.Claims; -using Bit.Api.Vault.AuthorizationHandlers.Groups; -using Bit.Core.Context; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using Microsoft.AspNetCore.Authorization; -using NSubstitute; -using Xunit; - -namespace Bit.Api.Test.Vault.AuthorizationHandlers; - -[SutProviderCustomize] -public class GroupAuthorizationHandlerTests -{ - [Theory] - [BitAutoData(OrganizationUserType.Admin)] - [BitAutoData(OrganizationUserType.Owner)] - [BitAutoData(OrganizationUserType.User)] - [BitAutoData(OrganizationUserType.Custom)] - public async Task CanReadAllAsync_WhenMemberOfOrg_Success( - OrganizationUserType userType, - Guid userId, SutProvider sutProvider, - CurrentContextOrganization organization) - { - organization.Type = userType; - organization.Permissions = new Permissions(); - - var context = new AuthorizationHandlerContext( - new[] { GroupOperations.ReadAll(organization.Id) }, - new ClaimsPrincipal(), - null); - - sutProvider.GetDependency().UserId.Returns(userId); - sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - - await sutProvider.Sut.HandleAsync(context); - - Assert.True(context.HasSucceeded); - } - - [Theory, BitAutoData] - public async Task CanReadAllAsync_WithProviderUser_Success( - Guid userId, - SutProvider sutProvider, CurrentContextOrganization organization) - { - organization.Type = OrganizationUserType.User; - organization.Permissions = new Permissions(); - - var context = new AuthorizationHandlerContext( - new[] { GroupOperations.ReadAll(organization.Id) }, - new ClaimsPrincipal(), - null); - - sutProvider.GetDependency() - .UserId - .Returns(userId); - sutProvider.GetDependency() - .ProviderUserForOrgAsync(organization.Id) - .Returns(true); - - await sutProvider.Sut.HandleAsync(context); - - Assert.True(context.HasSucceeded); - } - - [Theory, BitAutoData] - public async Task CanReadAllAsync_WhenMissingOrgAccess_NoSuccess( - Guid userId, - CurrentContextOrganization organization, - SutProvider sutProvider) - { - - var context = new AuthorizationHandlerContext( - new[] { GroupOperations.ReadAll(organization.Id) }, - new ClaimsPrincipal(), - null - ); - - sutProvider.GetDependency().UserId.Returns(userId); - sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); - sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - - await sutProvider.Sut.HandleAsync(context); - Assert.False(context.HasSucceeded); - } - - [Theory, BitAutoData] - public async Task HandleRequirementAsync_MissingUserId_Failure( - Guid organizationId, - SutProvider sutProvider) - { - var context = new AuthorizationHandlerContext( - new[] { GroupOperations.ReadAll(organizationId) }, - new ClaimsPrincipal(), - null - ); - - // Simulate missing user id - sutProvider.GetDependency().UserId.Returns((Guid?)null); - - await sutProvider.Sut.HandleAsync(context); - Assert.False(context.HasSucceeded); - Assert.True(context.HasFailed); - } - - [Theory, BitAutoData] - public async Task HandleRequirementAsync_NoSpecifiedOrgId_Failure( - SutProvider sutProvider) - { - var context = new AuthorizationHandlerContext( - new[] { GroupOperations.ReadAll(default) }, - new ClaimsPrincipal(), - null - ); - - sutProvider.GetDependency().UserId.Returns(new Guid()); - - await sutProvider.Sut.HandleAsync(context); - - Assert.False(context.HasSucceeded); - Assert.True(context.HasFailed); - } -} diff --git a/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserDetailsAuthorizationHandlerTests.cs b/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserDetailsAuthorizationHandlerTests.cs index 4a3a7f647af0..0814754f8b14 100644 --- a/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserDetailsAuthorizationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserDetailsAuthorizationHandlerTests.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Test.AdminConsole.AutoFixture; diff --git a/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandlerTests.cs b/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandlerTests.cs index c74cb2d5cada..6732486a545f 100644 --- a/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandlerTests.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Test.AdminConsole.AutoFixture; From a26ba3b3309d37ea0bd33fef0f1c77a9f5a98a8c Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:11:10 -0500 Subject: [PATCH 03/16] [PM-14401] Scale MSP on Admin client organization update (#5001) * Privatize GetAssignedSeatTotalAsync * Add SeatAdjustmentResultsInPurchase method * Move adjustment logic to ProviderClientsController.Update * Remove unused AssignSeatsToClientOrganization method * Alphabetize ProviderBillingService * Scale MSP on Admin client organization update * Run dotnet format * Patch build process * Rui's feedback --------- Co-authored-by: Matt Bishop --- .../Billing/ProviderBillingService.cs | 255 ++--- .../Billing/ProviderBillingServiceTests.cs | 993 +++++++----------- .../Controllers/OrganizationsController.cs | 89 +- .../Controllers/BaseBillingController.cs | 4 +- .../Controllers/ProviderClientsController.cs | 22 +- .../Services/IProviderBillingService.cs | 50 +- src/Core/Constants.cs | 1 + .../OrganizationsControllerTests.cs | 343 ++++++ .../ProviderClientsControllerTests.cs | 69 +- test/Api.Test/Billing/Utilities.cs | 4 +- 10 files changed, 1001 insertions(+), 829 deletions(-) create mode 100644 test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index 7b1d33599e59..a6bf62871f29 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -2,17 +2,14 @@ using Bit.Commercial.Core.Billing.Models; 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.Extensions; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Contracts; -using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; @@ -27,7 +24,6 @@ namespace Bit.Commercial.Core.Billing; public class ProviderBillingService( - ICurrentContext currentContext, IGlobalSettings globalSettings, ILogger logger, IOrganizationRepository organizationRepository, @@ -35,38 +31,76 @@ public class ProviderBillingService( IProviderInvoiceItemRepository providerInvoiceItemRepository, IProviderOrganizationRepository providerOrganizationRepository, IProviderPlanRepository providerPlanRepository, - IProviderRepository providerRepository, IStripeAdapter stripeAdapter, ISubscriberService subscriberService) : IProviderBillingService { - public async Task AssignSeatsToClientOrganization( - Provider provider, - Organization organization, - int seats) + public async Task ChangePlan(ChangeProviderPlanCommand command) { - ArgumentNullException.ThrowIfNull(organization); + var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId); - if (seats < 0) + if (plan == null) { - throw new BillingException( - "You cannot assign negative seats to a client.", - "MSP cannot assign negative seats to a client organization"); + throw new BadRequestException("Provider plan not found."); } - if (seats == organization.Seats) + if (plan.PlanType == command.NewPlan) { - logger.LogWarning("Client organization ({ID}) already has {Seats} seats assigned to it", organization.Id, organization.Seats); - return; } - var seatAdjustment = seats - (organization.Seats ?? 0); + var oldPlanConfiguration = StaticStore.GetPlan(plan.PlanType); - await ScaleSeats(provider, organization.PlanType, seatAdjustment); + plan.PlanType = command.NewPlan; + await providerPlanRepository.ReplaceAsync(plan); - organization.Seats = seats; + Subscription subscription; + try + { + subscription = await stripeAdapter.ProviderSubscriptionGetAsync(command.GatewaySubscriptionId, plan.ProviderId); + } + catch (InvalidOperationException) + { + throw new ConflictException("Subscription not found."); + } - await organizationRepository.ReplaceAsync(organization); + var oldSubscriptionItem = subscription.Items.SingleOrDefault(x => + x.Price.Id == oldPlanConfiguration.PasswordManager.StripeProviderPortalSeatPlanId); + + var updateOptions = new SubscriptionUpdateOptions + { + Items = + [ + new SubscriptionItemOptions + { + Price = StaticStore.GetPlan(command.NewPlan).PasswordManager.StripeProviderPortalSeatPlanId, + Quantity = oldSubscriptionItem!.Quantity + }, + new SubscriptionItemOptions + { + Id = oldSubscriptionItem.Id, + Deleted = true + } + ] + }; + + await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId, updateOptions); + + // Refactor later to ?ChangeClientPlanCommand? (ProviderPlanId, ProviderId, OrganizationId) + // 1. Retrieve PlanType and PlanName for ProviderPlan + // 2. Assign PlanType & PlanName to Organization + var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(plan.ProviderId); + + foreach (var providerOrganization in providerOrganizations) + { + var organization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId); + if (organization == null) + { + throw new ConflictException($"Organization '{providerOrganization.Id}' not found."); + } + organization.PlanType = command.NewPlan; + organization.Plan = StaticStore.GetPlan(command.NewPlan).Name; + await organizationRepository.ReplaceAsync(organization); + } } public async Task CreateCustomerForClientOrganization( @@ -171,65 +205,16 @@ public async Task GenerateClientInvoiceReport( return memoryStream.ToArray(); } - public async Task GetAssignedSeatTotalForPlanOrThrow( - Guid providerId, - PlanType planType) - { - var provider = await providerRepository.GetByIdAsync(providerId); - - if (provider == null) - { - logger.LogError( - "Could not find provider ({ID}) when retrieving assigned seat total", - providerId); - - throw new BillingException(); - } - - if (provider.Type == ProviderType.Reseller) - { - logger.LogError("Assigned seats cannot be retrieved for reseller-type provider ({ID})", providerId); - - throw new BillingException(); - } - - var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId); - - var plan = StaticStore.GetPlan(planType); - - return providerOrganizations - .Where(providerOrganization => providerOrganization.Plan == plan.Name && providerOrganization.Status == OrganizationStatusType.Managed) - .Sum(providerOrganization => providerOrganization.Seats ?? 0); - } - public async Task ScaleSeats( Provider provider, PlanType planType, int seatAdjustment) { - ArgumentNullException.ThrowIfNull(provider); + var providerPlan = await GetProviderPlanAsync(provider, planType); - if (!provider.SupportsConsolidatedBilling()) - { - logger.LogError("Provider ({ProviderID}) cannot scale their seats", provider.Id); + var seatMinimum = providerPlan.SeatMinimum ?? 0; - throw new BillingException(); - } - - var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); - - var providerPlan = providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == planType); - - if (providerPlan == null || !providerPlan.IsConfigured()) - { - logger.LogError("Cannot scale provider ({ProviderID}) seats for plan type {PlanType} when their matching provider plan is not configured", provider.Id, planType); - - throw new BillingException(); - } - - var seatMinimum = providerPlan.SeatMinimum.GetValueOrDefault(0); - - var currentlyAssignedSeatTotal = await GetAssignedSeatTotalForPlanOrThrow(provider.Id, planType); + var currentlyAssignedSeatTotal = await GetAssignedSeatTotalAsync(provider, planType); var newlyAssignedSeatTotal = currentlyAssignedSeatTotal + seatAdjustment; @@ -256,13 +241,6 @@ public async Task ScaleSeats( else if (currentlyAssignedSeatTotal <= seatMinimum && newlyAssignedSeatTotal > seatMinimum) { - if (!currentContext.ProviderProviderAdmin(provider.Id)) - { - logger.LogError("Service user for provider ({ProviderID}) cannot scale a provider's seat count over the seat minimum", provider.Id); - - throw new BillingException(); - } - await update( seatMinimum, newlyAssignedSeatTotal); @@ -291,6 +269,26 @@ await update( } } + public async Task SeatAdjustmentResultsInPurchase( + Provider provider, + PlanType planType, + int seatAdjustment) + { + var providerPlan = await GetProviderPlanAsync(provider, planType); + + var seatMinimum = providerPlan.SeatMinimum; + + var currentlyAssignedSeatTotal = await GetAssignedSeatTotalAsync(provider, planType); + + var newlyAssignedSeatTotal = currentlyAssignedSeatTotal + seatAdjustment; + + return + // Below the limit to above the limit + (currentlyAssignedSeatTotal <= seatMinimum && newlyAssignedSeatTotal > seatMinimum) || + // Above the limit to further above the limit + (currentlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > currentlyAssignedSeatTotal); + } + public async Task SetupCustomer( Provider provider, TaxInfo taxInfo) @@ -431,75 +429,6 @@ public async Task SetupSubscription( } } - public async Task ChangePlan(ChangeProviderPlanCommand command) - { - var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId); - - if (plan == null) - { - throw new BadRequestException("Provider plan not found."); - } - - if (plan.PlanType == command.NewPlan) - { - return; - } - - var oldPlanConfiguration = StaticStore.GetPlan(plan.PlanType); - - plan.PlanType = command.NewPlan; - await providerPlanRepository.ReplaceAsync(plan); - - Subscription subscription; - try - { - subscription = await stripeAdapter.ProviderSubscriptionGetAsync(command.GatewaySubscriptionId, plan.ProviderId); - } - catch (InvalidOperationException) - { - throw new ConflictException("Subscription not found."); - } - - var oldSubscriptionItem = subscription.Items.SingleOrDefault(x => - x.Price.Id == oldPlanConfiguration.PasswordManager.StripeProviderPortalSeatPlanId); - - var updateOptions = new SubscriptionUpdateOptions - { - Items = - [ - new SubscriptionItemOptions - { - Price = StaticStore.GetPlan(command.NewPlan).PasswordManager.StripeProviderPortalSeatPlanId, - Quantity = oldSubscriptionItem!.Quantity - }, - new SubscriptionItemOptions - { - Id = oldSubscriptionItem.Id, - Deleted = true - } - ] - }; - - await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId, updateOptions); - - // Refactor later to ?ChangeClientPlanCommand? (ProviderPlanId, ProviderId, OrganizationId) - // 1. Retrieve PlanType and PlanName for ProviderPlan - // 2. Assign PlanType & PlanName to Organization - var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(plan.ProviderId); - - foreach (var providerOrganization in providerOrganizations) - { - var organization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId); - if (organization == null) - { - throw new ConflictException($"Organization '{providerOrganization.Id}' not found."); - } - organization.PlanType = command.NewPlan; - organization.Plan = StaticStore.GetPlan(command.NewPlan).Name; - await organizationRepository.ReplaceAsync(organization); - } - } - public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command) { if (command.Configuration.Any(x => x.SeatsMinimum < 0)) @@ -610,4 +539,32 @@ await paymentService.AdjustSeats( await providerPlanRepository.ReplaceAsync(providerPlan); }; + + // TODO: Replace with SPROC + private async Task GetAssignedSeatTotalAsync(Provider provider, PlanType planType) + { + var providerOrganizations = + await providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id); + + var plan = StaticStore.GetPlan(planType); + + return providerOrganizations + .Where(providerOrganization => providerOrganization.Plan == plan.Name && providerOrganization.Status == OrganizationStatusType.Managed) + .Sum(providerOrganization => providerOrganization.Seats ?? 0); + } + + // TODO: Replace with SPROC + private async Task GetProviderPlanAsync(Provider provider, PlanType planType) + { + var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); + + var providerPlan = providerPlans.FirstOrDefault(x => x.PlanType == planType); + + if (providerPlan == null || !providerPlan.IsConfigured()) + { + throw new BillingException(message: "Provider plan is missing or misconfigured"); + } + + return providerPlan; + } } diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs index 7c3e8cad8764..881a9845545b 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs @@ -4,17 +4,14 @@ using Bit.Commercial.Core.Billing.Models; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.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.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Contracts; -using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -36,170 +33,341 @@ namespace Bit.Commercial.Core.Test.Billing; [SutProviderCustomize] public class ProviderBillingServiceTests { - #region AssignSeatsToClientOrganization & ScaleSeats + #region ChangePlan [Theory, BitAutoData] - public Task AssignSeatsToClientOrganization_NullProvider_ArgumentNullException( - Organization organization, - int seats, + public async Task ChangePlan_NullProviderPlan_ThrowsBadRequestException( + ChangeProviderPlanCommand command, SutProvider sutProvider) - => Assert.ThrowsAsync(() => - sutProvider.Sut.AssignSeatsToClientOrganization(null, organization, seats)); + { + // Arrange + var providerPlanRepository = sutProvider.GetDependency(); + providerPlanRepository.GetByIdAsync(Arg.Any()).Returns((ProviderPlan)null); + + // Act + var actual = await Assert.ThrowsAsync(() => sutProvider.Sut.ChangePlan(command)); + + // Assert + Assert.Equal("Provider plan not found.", actual.Message); + } [Theory, BitAutoData] - public Task AssignSeatsToClientOrganization_NullOrganization_ArgumentNullException( - Provider provider, - int seats, + public async Task ChangePlan_ProviderNotFound_DoesNothing( + ChangeProviderPlanCommand command, SutProvider sutProvider) - => Assert.ThrowsAsync(() => - sutProvider.Sut.AssignSeatsToClientOrganization(provider, null, seats)); + { + // Arrange + var providerPlanRepository = sutProvider.GetDependency(); + var stripeAdapter = sutProvider.GetDependency(); + var existingPlan = new ProviderPlan + { + Id = command.ProviderPlanId, + PlanType = command.NewPlan, + PurchasedSeats = 0, + AllocatedSeats = 0, + SeatMinimum = 0 + }; + providerPlanRepository + .GetByIdAsync(Arg.Is(p => p == command.ProviderPlanId)) + .Returns(existingPlan); + + // Act + await sutProvider.Sut.ChangePlan(command); + + // Assert + await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any()); + await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any(), Arg.Any()); + } [Theory, BitAutoData] - public Task AssignSeatsToClientOrganization_NegativeSeats_BillingException( - Provider provider, - Organization organization, + public async Task ChangePlan_SameProviderPlan_DoesNothing( + ChangeProviderPlanCommand command, SutProvider sutProvider) - => Assert.ThrowsAsync(() => - sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, -5)); + { + // Arrange + var providerPlanRepository = sutProvider.GetDependency(); + var stripeAdapter = sutProvider.GetDependency(); + var existingPlan = new ProviderPlan + { + Id = command.ProviderPlanId, + PlanType = command.NewPlan, + PurchasedSeats = 0, + AllocatedSeats = 0, + SeatMinimum = 0 + }; + providerPlanRepository + .GetByIdAsync(Arg.Is(p => p == command.ProviderPlanId)) + .Returns(existingPlan); + + // Act + await sutProvider.Sut.ChangePlan(command); + + // Assert + await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any()); + await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any(), Arg.Any()); + } [Theory, BitAutoData] - public async Task AssignSeatsToClientOrganization_CurrentSeatsMatchesNewSeats_NoOp( + public async Task ChangePlan_UpdatesSubscriptionCorrectly( + Guid providerPlanId, Provider provider, - Organization organization, - int seats, SutProvider sutProvider) { - organization.PlanType = PlanType.TeamsMonthly; + // Arrange + var providerPlanRepository = sutProvider.GetDependency(); + var existingPlan = new ProviderPlan + { + Id = providerPlanId, + ProviderId = provider.Id, + PlanType = PlanType.EnterpriseAnnually, + PurchasedSeats = 2, + AllocatedSeats = 10, + SeatMinimum = 8 + }; + providerPlanRepository + .GetByIdAsync(Arg.Is(p => p == providerPlanId)) + .Returns(existingPlan); - organization.Seats = seats; + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter.ProviderSubscriptionGetAsync( + Arg.Is(provider.GatewaySubscriptionId), + Arg.Is(provider.Id)) + .Returns(new Subscription + { + Id = provider.GatewaySubscriptionId, + Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + Id = "si_ent_annual", + Price = new Price + { + Id = StaticStore.GetPlan(PlanType.EnterpriseAnnually).PasswordManager + .StripeProviderPortalSeatPlanId + }, + Quantity = 10 + } + ] + } + }); + + var command = + new ChangeProviderPlanCommand(providerPlanId, PlanType.EnterpriseMonthly, provider.GatewaySubscriptionId); + + // Act + await sutProvider.Sut.ChangePlan(command); + + // Assert + await providerPlanRepository.Received(1) + .ReplaceAsync(Arg.Is(p => p.PlanType == PlanType.EnterpriseMonthly)); - await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats); + await stripeAdapter.Received(1) + .SubscriptionUpdateAsync( + Arg.Is(provider.GatewaySubscriptionId), + Arg.Is(p => + p.Items.Count(si => si.Id == "si_ent_annual" && si.Deleted == true) == 1)); - await sutProvider.GetDependency().DidNotReceive().GetByProviderId(provider.Id); + var newPlanCfg = StaticStore.GetPlan(command.NewPlan); + await stripeAdapter.Received(1) + .SubscriptionUpdateAsync( + Arg.Is(provider.GatewaySubscriptionId), + Arg.Is(p => + p.Items.Count(si => + si.Price == newPlanCfg.PasswordManager.StripeProviderPortalSeatPlanId && + si.Deleted == default && + si.Quantity == 10) == 1)); } + #endregion + + #region CreateCustomerForClientOrganization + [Theory, BitAutoData] - public async Task - AssignSeatsToClientOrganization_OrganizationPlanTypeDoesNotSupportConsolidatedBilling_ContactSupport( - Provider provider, - Organization organization, - int seats, - SutProvider sutProvider) - { - organization.PlanType = PlanType.FamiliesAnnually; + public async Task CreateCustomerForClientOrganization_ProviderNull_ThrowsArgumentNullException( + Organization organization, + SutProvider sutProvider) => + await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateCustomerForClientOrganization(null, organization)); - await ThrowsBillingExceptionAsync(() => - sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats)); - } + [Theory, BitAutoData] + public async Task CreateCustomerForClientOrganization_OrganizationNull_ThrowsArgumentNullException( + Provider provider, + SutProvider sutProvider) => + await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateCustomerForClientOrganization(provider, null)); [Theory, BitAutoData] - public async Task AssignSeatsToClientOrganization_ProviderPlanIsNotConfigured_ContactSupport( + public async Task CreateCustomerForClientOrganization_HasGatewayCustomerId_NoOp( Provider provider, Organization organization, - int seats, SutProvider sutProvider) { - organization.PlanType = PlanType.TeamsMonthly; + organization.GatewayCustomerId = "customer_id"; - sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(new List - { - new() { Id = Guid.NewGuid(), PlanType = PlanType.TeamsMonthly, ProviderId = provider.Id } - }); + await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization); - await ThrowsBillingExceptionAsync(() => - sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats)); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .GetCustomerOrThrow(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] - public async Task AssignSeatsToClientOrganization_BelowToBelow_Succeeds( + public async Task CreateCustomer_ForClientOrg_Succeeds( Provider provider, Organization organization, SutProvider sutProvider) { - organization.Seats = 10; - - organization.PlanType = PlanType.TeamsMonthly; - - // Scale up 10 seats - const int seats = 20; + organization.GatewayCustomerId = null; + organization.Name = "Name"; + organization.BusinessName = "BusinessName"; - var providerPlans = new List + var providerCustomer = new Customer { - new() + Address = new Address { - Id = Guid.NewGuid(), - PlanType = PlanType.TeamsMonthly, - ProviderId = provider.Id, - PurchasedSeats = 0, - // 100 minimum - SeatMinimum = 100, - AllocatedSeats = 50 + Country = "USA", + PostalCode = "12345", + Line1 = "123 Main St.", + Line2 = "Unit 4", + City = "Fake Town", + State = "Fake State" }, - new() + TaxIds = new StripeList { - Id = Guid.NewGuid(), - PlanType = PlanType.EnterpriseMonthly, - ProviderId = provider.Id, - PurchasedSeats = 0, - SeatMinimum = 500, - AllocatedSeats = 0 + Data = + [ + new TaxId { Type = "TYPE", Value = "VALUE" } + ] } }; - var providerPlan = providerPlans.First(); + sutProvider.GetDependency().GetCustomerOrThrow(provider, Arg.Is( + options => options.Expand.FirstOrDefault() == "tax_ids")) + .Returns(providerCustomer); - sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); + sutProvider.GetDependency().BaseServiceUri + .Returns(new Bit.Core.Settings.GlobalSettings.BaseServiceUriSettings(new Bit.Core.Settings.GlobalSettings()) + { + CloudRegion = "US" + }); - // 50 seats currently assigned with a seat minimum of 100 - sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); + sutProvider.GetDependency().CustomerCreateAsync(Arg.Is( + options => + options.Address.Country == providerCustomer.Address.Country && + options.Address.PostalCode == providerCustomer.Address.PostalCode && + options.Address.Line1 == providerCustomer.Address.Line1 && + options.Address.Line2 == providerCustomer.Address.Line2 && + options.Address.City == providerCustomer.Address.City && + options.Address.State == providerCustomer.Address.State && + options.Name == organization.DisplayName() && + options.Description == $"{provider.Name} Client Organization" && + options.Email == provider.BillingEmail && + options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" && + options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" && + options.Metadata["region"] == "US" && + options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type && + options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value)) + .Returns(new Customer { Id = "customer_id" }); - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization); - sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( - [ - new ProviderOrganizationOrganizationDetails - { - Plan = teamsMonthlyPlan.Name, - Status = OrganizationStatusType.Managed, - Seats = 25 - }, - new ProviderOrganizationOrganizationDetails + await sutProvider.GetDependency().Received(1).CustomerCreateAsync(Arg.Is( + options => + options.Address.Country == providerCustomer.Address.Country && + options.Address.PostalCode == providerCustomer.Address.PostalCode && + options.Address.Line1 == providerCustomer.Address.Line1 && + options.Address.Line2 == providerCustomer.Address.Line2 && + options.Address.City == providerCustomer.Address.City && + options.Address.State == providerCustomer.Address.State && + options.Name == organization.DisplayName() && + options.Description == $"{provider.Name} Client Organization" && + options.Email == provider.BillingEmail && + options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" && + options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" && + options.Metadata["region"] == "US" && + options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type && + options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value)); + + await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( + org => org.GatewayCustomerId == "customer_id")); + } + + #endregion + + #region GenerateClientInvoiceReport + + [Theory, BitAutoData] + public async Task GenerateClientInvoiceReport_NullInvoiceId_ThrowsArgumentNullException( + SutProvider sutProvider) => + await Assert.ThrowsAsync(() => sutProvider.Sut.GenerateClientInvoiceReport(null)); + + [Theory, BitAutoData] + public async Task GenerateClientInvoiceReport_NoInvoiceItems_ReturnsNull( + string invoiceId, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByInvoiceId(invoiceId).Returns([]); + + var reportContent = await sutProvider.Sut.GenerateClientInvoiceReport(invoiceId); + + Assert.Null(reportContent); + } + + [Theory, BitAutoData] + public async Task GenerateClientInvoiceReport_Succeeds( + string invoiceId, + SutProvider sutProvider) + { + var clientId = Guid.NewGuid(); + + var invoiceItems = new List + { + new () { - Plan = teamsMonthlyPlan.Name, - Status = OrganizationStatusType.Managed, - Seats = 25 + ClientId = clientId, + ClientName = "Client 1", + AssignedSeats = 50, + UsedSeats = 30, + PlanName = "Teams (Monthly)", + Total = 500 } - ]); + }; - await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats); + sutProvider.GetDependency().GetByInvoiceId(invoiceId).Returns(invoiceItems); - // 50 assigned seats + 10 seat scale up = 60 seats, well below the 100 minimum - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().AdjustSeats( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()); + var reportContent = await sutProvider.Sut.GenerateClientInvoiceReport(invoiceId); + + using var memoryStream = new MemoryStream(reportContent); + + using var streamReader = new StreamReader(memoryStream); + + using var csvReader = new CsvReader(streamReader, CultureInfo.InvariantCulture); + + var records = csvReader.GetRecords().ToList(); + + Assert.Single(records); - await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( - org => org.Id == organization.Id && org.Seats == seats)); + var record = records.First(); - await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( - pPlan => pPlan.AllocatedSeats == 60)); + Assert.Equal(clientId.ToString(), record.Id); + Assert.Equal("Client 1", record.Client); + Assert.Equal(50, record.Assigned); + Assert.Equal(30, record.Used); + Assert.Equal(20, record.Remaining); + Assert.Equal("Teams (Monthly)", record.Plan); + Assert.Equal("$500.00", record.Total); } + #endregion + + #region ScaleSeats + [Theory, BitAutoData] - public async Task AssignSeatsToClientOrganization_BelowToAbove_NotProviderAdmin_ContactSupport( + public async Task ScaleSeats_BelowToBelow_Succeeds( Provider provider, - Organization organization, SutProvider sutProvider) { - organization.Seats = 10; - - organization.PlanType = PlanType.TeamsMonthly; - - // Scale up 10 seats - const int seats = 20; - var providerPlans = new List { new() @@ -208,9 +376,8 @@ public async Task AssignSeatsToClientOrganization_BelowToAbove_NotProviderAdmin_ PlanType = PlanType.TeamsMonthly, ProviderId = provider.Id, PurchasedSeats = 0, - // 100 minimum SeatMinimum = 100, - AllocatedSeats = 95 + AllocatedSeats = 50 }, new() { @@ -225,9 +392,7 @@ public async Task AssignSeatsToClientOrganization_BelowToAbove_NotProviderAdmin_ sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); - // 95 seats currently assigned with a seat minimum of 100 - sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); - + // 50 seats currently assigned with a seat minimum of 100 var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( @@ -236,35 +401,34 @@ public async Task AssignSeatsToClientOrganization_BelowToAbove_NotProviderAdmin_ { Plan = teamsMonthlyPlan.Name, Status = OrganizationStatusType.Managed, - Seats = 60 + Seats = 25 }, new ProviderOrganizationOrganizationDetails { Plan = teamsMonthlyPlan.Name, Status = OrganizationStatusType.Managed, - Seats = 35 + Seats = 25 } ]); - sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(false); + await sutProvider.Sut.ScaleSeats(provider, PlanType.TeamsMonthly, 10); + + // 50 assigned seats + 10 seat scale up = 60 seats, well below the 100 minimum + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().AdjustSeats( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()); - await ThrowsBillingExceptionAsync(() => - sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats)); + await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( + pPlan => pPlan.AllocatedSeats == 60)); } [Theory, BitAutoData] - public async Task AssignSeatsToClientOrganization_BelowToAbove_Succeeds( + public async Task ScaleSeats_BelowToAbove_Succeeds( Provider provider, - Organization organization, SutProvider sutProvider) { - organization.Seats = 10; - - organization.PlanType = PlanType.TeamsMonthly; - - // Scale up 10 seats - const int seats = 20; - var providerPlans = new List { new() @@ -273,7 +437,6 @@ public async Task AssignSeatsToClientOrganization_BelowToAbove_Succeeds( PlanType = PlanType.TeamsMonthly, ProviderId = provider.Id, PurchasedSeats = 0, - // 100 minimum SeatMinimum = 100, AllocatedSeats = 95 }, @@ -293,8 +456,6 @@ public async Task AssignSeatsToClientOrganization_BelowToAbove_Succeeds( sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); // 95 seats currently assigned with a seat minimum of 100 - sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( @@ -313,9 +474,7 @@ public async Task AssignSeatsToClientOrganization_BelowToAbove_Succeeds( } ]); - sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); - - await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats); + await sutProvider.Sut.ScaleSeats(provider, PlanType.TeamsMonthly, 10); // 95 current + 10 seat scale = 105 seats, 5 above the minimum await sutProvider.GetDependency().Received(1).AdjustSeats( @@ -324,29 +483,16 @@ await sutProvider.GetDependency().Received(1).AdjustSeats( providerPlan.SeatMinimum!.Value, 105); - await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( - org => org.Id == organization.Id && org.Seats == seats)); - // 105 total seats - 100 minimum = 5 purchased seats await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 5 && pPlan.AllocatedSeats == 105)); } [Theory, BitAutoData] - public async Task AssignSeatsToClientOrganization_AboveToAbove_Succeeds( + public async Task ScaleSeats_AboveToAbove_Succeeds( Provider provider, - Organization organization, SutProvider sutProvider) { - provider.Type = ProviderType.Msp; - - organization.Seats = 10; - - organization.PlanType = PlanType.TeamsMonthly; - - // Scale up 10 seats - const int seats = 20; - var providerPlans = new List { new() @@ -354,9 +500,7 @@ public async Task AssignSeatsToClientOrganization_AboveToAbove_Succeeds( Id = Guid.NewGuid(), PlanType = PlanType.TeamsMonthly, ProviderId = provider.Id, - // 10 additional purchased seats PurchasedSeats = 10, - // 100 seat minimum SeatMinimum = 100, AllocatedSeats = 110 }, @@ -376,8 +520,6 @@ public async Task AssignSeatsToClientOrganization_AboveToAbove_Succeeds( sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); // 110 seats currently assigned with a seat minimum of 100 - sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( @@ -396,7 +538,7 @@ public async Task AssignSeatsToClientOrganization_AboveToAbove_Succeeds( } ]); - await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats); + await sutProvider.Sut.ScaleSeats(provider, PlanType.TeamsMonthly, 10); // 110 current + 10 seat scale up = 120 seats await sutProvider.GetDependency().Received(1).AdjustSeats( @@ -405,334 +547,149 @@ await sutProvider.GetDependency().Received(1).AdjustSeats( 110, 120); - await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( - org => org.Id == organization.Id && org.Seats == seats)); - // 120 total seats - 100 seat minimum = 20 purchased seats await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 20 && pPlan.AllocatedSeats == 120)); } [Theory, BitAutoData] - public async Task AssignSeatsToClientOrganization_AboveToBelow_Succeeds( - Provider provider, - Organization organization, - SutProvider sutProvider) - { - organization.Seats = 50; - - organization.PlanType = PlanType.TeamsMonthly; - - // Scale down 30 seats - const int seats = 20; - - var providerPlans = new List - { - new() - { - Id = Guid.NewGuid(), - PlanType = PlanType.TeamsMonthly, - ProviderId = provider.Id, - // 10 additional purchased seats - PurchasedSeats = 10, - // 100 seat minimum - SeatMinimum = 100, - AllocatedSeats = 110 - }, - new() - { - Id = Guid.NewGuid(), - PlanType = PlanType.EnterpriseMonthly, - ProviderId = provider.Id, - PurchasedSeats = 0, - SeatMinimum = 500, - AllocatedSeats = 0 - } - }; - - var providerPlan = providerPlans.First(); - - sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); - - // 110 seats currently assigned with a seat minimum of 100 - sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); - - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - - sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( - [ - new ProviderOrganizationOrganizationDetails - { - Plan = teamsMonthlyPlan.Name, - Status = OrganizationStatusType.Managed, - Seats = 60 - }, - new ProviderOrganizationOrganizationDetails - { - Plan = teamsMonthlyPlan.Name, - Status = OrganizationStatusType.Managed, - Seats = 50 - } - ]); - - await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats); - - // 110 seats - 30 scale down seats = 80 seats, below the 100 seat minimum. - await sutProvider.GetDependency().Received(1).AdjustSeats( - provider, - StaticStore.GetPlan(providerPlan.PlanType), - 110, - providerPlan.SeatMinimum!.Value); - - await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( - org => org.Id == organization.Id && org.Seats == seats)); - - // Being below the seat minimum means no purchased seats. - await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( - pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 0 && pPlan.AllocatedSeats == 80)); - } - - #endregion - - #region CreateCustomerForClientOrganization - - [Theory, BitAutoData] - public async Task CreateCustomerForClientOrganization_ProviderNull_ThrowsArgumentNullException( - Organization organization, - SutProvider sutProvider) => - await Assert.ThrowsAsync(() => - sutProvider.Sut.CreateCustomerForClientOrganization(null, organization)); - - [Theory, BitAutoData] - public async Task CreateCustomerForClientOrganization_OrganizationNull_ThrowsArgumentNullException( - Provider provider, - SutProvider sutProvider) => - await Assert.ThrowsAsync(() => - sutProvider.Sut.CreateCustomerForClientOrganization(provider, null)); - - [Theory, BitAutoData] - public async Task CreateCustomerForClientOrganization_HasGatewayCustomerId_NoOp( - Provider provider, - Organization organization, - SutProvider sutProvider) - { - organization.GatewayCustomerId = "customer_id"; - - await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .GetCustomerOrThrow(Arg.Any(), Arg.Any()); - } - - [Theory, BitAutoData] - public async Task CreateCustomer_ForClientOrg_Succeeds( + public async Task ScaleSeats_AboveToBelow_Succeeds( Provider provider, - Organization organization, - SutProvider sutProvider) - { - organization.GatewayCustomerId = null; - organization.Name = "Name"; - organization.BusinessName = "BusinessName"; - - var providerCustomer = new Customer - { - Address = new Address - { - Country = "USA", - PostalCode = "12345", - Line1 = "123 Main St.", - Line2 = "Unit 4", - City = "Fake Town", - State = "Fake State" - }, - TaxIds = new StripeList - { - Data = - [ - new TaxId { Type = "TYPE", Value = "VALUE" } - ] - } - }; - - sutProvider.GetDependency().GetCustomerOrThrow(provider, Arg.Is( - options => options.Expand.FirstOrDefault() == "tax_ids")) - .Returns(providerCustomer); - - sutProvider.GetDependency().BaseServiceUri - .Returns(new Bit.Core.Settings.GlobalSettings.BaseServiceUriSettings(new Bit.Core.Settings.GlobalSettings()) - { - CloudRegion = "US" - }); - - sutProvider.GetDependency().CustomerCreateAsync(Arg.Is( - options => - options.Address.Country == providerCustomer.Address.Country && - options.Address.PostalCode == providerCustomer.Address.PostalCode && - options.Address.Line1 == providerCustomer.Address.Line1 && - options.Address.Line2 == providerCustomer.Address.Line2 && - options.Address.City == providerCustomer.Address.City && - options.Address.State == providerCustomer.Address.State && - options.Name == organization.DisplayName() && - options.Description == $"{provider.Name} Client Organization" && - options.Email == provider.BillingEmail && - options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" && - options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" && - options.Metadata["region"] == "US" && - options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type && - options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value)) - .Returns(new Customer { Id = "customer_id" }); - - await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization); - - await sutProvider.GetDependency().Received(1).CustomerCreateAsync(Arg.Is( - options => - options.Address.Country == providerCustomer.Address.Country && - options.Address.PostalCode == providerCustomer.Address.PostalCode && - options.Address.Line1 == providerCustomer.Address.Line1 && - options.Address.Line2 == providerCustomer.Address.Line2 && - options.Address.City == providerCustomer.Address.City && - options.Address.State == providerCustomer.Address.State && - options.Name == organization.DisplayName() && - options.Description == $"{provider.Name} Client Organization" && - options.Email == provider.BillingEmail && - options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" && - options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" && - options.Metadata["region"] == "US" && - options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type && - options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value)); - - await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( - org => org.GatewayCustomerId == "customer_id")); - } - - #endregion - - #region GenerateClientInvoiceReport - - [Theory, BitAutoData] - public async Task GenerateClientInvoiceReport_NullInvoiceId_ThrowsArgumentNullException( - SutProvider sutProvider) => - await Assert.ThrowsAsync(() => sutProvider.Sut.GenerateClientInvoiceReport(null)); - - [Theory, BitAutoData] - public async Task GenerateClientInvoiceReport_NoInvoiceItems_ReturnsNull( - string invoiceId, - SutProvider sutProvider) - { - sutProvider.GetDependency().GetByInvoiceId(invoiceId).Returns([]); - - var reportContent = await sutProvider.Sut.GenerateClientInvoiceReport(invoiceId); - - Assert.Null(reportContent); - } - - [Theory, BitAutoData] - public async Task GenerateClientInvoiceReport_Succeeds( - string invoiceId, SutProvider sutProvider) { - var clientId = Guid.NewGuid(); - - var invoiceItems = new List + var providerPlans = new List { - new () + new() { - ClientId = clientId, - ClientName = "Client 1", - AssignedSeats = 50, - UsedSeats = 30, - PlanName = "Teams (Monthly)", - Total = 500 + Id = Guid.NewGuid(), + PlanType = PlanType.TeamsMonthly, + ProviderId = provider.Id, + PurchasedSeats = 10, + SeatMinimum = 100, + AllocatedSeats = 110 + }, + new() + { + Id = Guid.NewGuid(), + PlanType = PlanType.EnterpriseMonthly, + ProviderId = provider.Id, + PurchasedSeats = 0, + SeatMinimum = 500, + AllocatedSeats = 0 } }; - sutProvider.GetDependency().GetByInvoiceId(invoiceId).Returns(invoiceItems); - - var reportContent = await sutProvider.Sut.GenerateClientInvoiceReport(invoiceId); - - using var memoryStream = new MemoryStream(reportContent); + var providerPlan = providerPlans.First(); - using var streamReader = new StreamReader(memoryStream); + sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); - using var csvReader = new CsvReader(streamReader, CultureInfo.InvariantCulture); + // 110 seats currently assigned with a seat minimum of 100 + var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - var records = csvReader.GetRecords().ToList(); + sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( + [ + new ProviderOrganizationOrganizationDetails + { + Plan = teamsMonthlyPlan.Name, + Status = OrganizationStatusType.Managed, + Seats = 60 + }, + new ProviderOrganizationOrganizationDetails + { + Plan = teamsMonthlyPlan.Name, + Status = OrganizationStatusType.Managed, + Seats = 50 + } + ]); - Assert.Single(records); + await sutProvider.Sut.ScaleSeats(provider, PlanType.TeamsMonthly, -30); - var record = records.First(); + // 110 seats - 30 scale down seats = 80 seats, below the 100 seat minimum. + await sutProvider.GetDependency().Received(1).AdjustSeats( + provider, + StaticStore.GetPlan(providerPlan.PlanType), + 110, + providerPlan.SeatMinimum!.Value); - Assert.Equal(clientId.ToString(), record.Id); - Assert.Equal("Client 1", record.Client); - Assert.Equal(50, record.Assigned); - Assert.Equal(30, record.Used); - Assert.Equal(20, record.Remaining); - Assert.Equal("Teams (Monthly)", record.Plan); - Assert.Equal("$500.00", record.Total); + // Being below the seat minimum means no purchased seats. + await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( + pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 0 && pPlan.AllocatedSeats == 80)); } #endregion - #region GetAssignedSeatTotalForPlanOrThrow + #region SeatAdjustmentResultsInPurchase [Theory, BitAutoData] - public async Task GetAssignedSeatTotalForPlanOrThrow_NullProvider_ContactSupport( - Guid providerId, - SutProvider sutProvider) - => await ThrowsBillingExceptionAsync(() => - sutProvider.Sut.GetAssignedSeatTotalForPlanOrThrow(providerId, PlanType.TeamsMonthly)); - - [Theory, BitAutoData] - public async Task GetAssignedSeatTotalForPlanOrThrow_ResellerProvider_ContactSupport( - Guid providerId, + public async Task SeatAdjustmentResultsInPurchase_BelowToAbove_True( Provider provider, + PlanType planType, SutProvider sutProvider) { - provider.Type = ProviderType.Reseller; + sutProvider.GetDependency().GetByProviderId(provider.Id).Returns([ + new ProviderPlan + { + PlanType = planType, + SeatMinimum = 10, + AllocatedSeats = 0, + PurchasedSeats = 0 + } + ]); + + sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( + [ + new ProviderOrganizationOrganizationDetails + { + Plan = StaticStore.GetPlan(planType).Name, + Status = OrganizationStatusType.Managed, + Seats = 5 + } + ]); - sutProvider.GetDependency().GetByIdAsync(providerId).Returns(provider); + const int seatAdjustment = 10; + + var result = await sutProvider.Sut.SeatAdjustmentResultsInPurchase( + provider, + planType, + seatAdjustment); - await ThrowsBillingExceptionAsync( - () => sutProvider.Sut.GetAssignedSeatTotalForPlanOrThrow(providerId, PlanType.TeamsMonthly)); + Assert.True(result); } [Theory, BitAutoData] - public async Task GetAssignedSeatTotalForPlanOrThrow_Succeeds( - Guid providerId, + public async Task SeatAdjustmentResultsInPurchase_AboveToFurtherAbove_True( Provider provider, + PlanType planType, SutProvider sutProvider) { - provider.Type = ProviderType.Msp; - - sutProvider.GetDependency().GetByIdAsync(providerId).Returns(provider); - - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly); - - var providerOrganizationOrganizationDetailList = new List - { - new() { Plan = teamsMonthlyPlan.Name, Status = OrganizationStatusType.Managed, Seats = 10 }, - new() { Plan = teamsMonthlyPlan.Name, Status = OrganizationStatusType.Managed, Seats = 10 }, - new() + sutProvider.GetDependency().GetByProviderId(provider.Id).Returns([ + new ProviderPlan { - // Ignored because of status. - Plan = teamsMonthlyPlan.Name, Status = OrganizationStatusType.Created, Seats = 100 - }, - new() + PlanType = planType, + SeatMinimum = 10, + AllocatedSeats = 0, + PurchasedSeats = 5 + } + ]); + + sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( + [ + new ProviderOrganizationOrganizationDetails { - // Ignored because of plan. - Plan = enterpriseMonthlyPlan.Name, Status = OrganizationStatusType.Managed, Seats = 30 + Plan = StaticStore.GetPlan(planType).Name, + Status = OrganizationStatusType.Managed, + Seats = 15 } - }; + ]); - sutProvider.GetDependency() - .GetManyDetailsByProviderAsync(providerId) - .Returns(providerOrganizationOrganizationDetailList); + const int seatAdjustment = 5; - var assignedSeatTotal = - await sutProvider.Sut.GetAssignedSeatTotalForPlanOrThrow(providerId, PlanType.TeamsMonthly); + var result = await sutProvider.Sut.SeatAdjustmentResultsInPurchase( + provider, + planType, + seatAdjustment); - Assert.Equal(20, assignedSeatTotal); + Assert.True(result); } #endregion @@ -1012,158 +969,6 @@ public async Task SetupSubscription_Succeeds( #endregion - #region ChangePlan - - [Theory, BitAutoData] - public async Task ChangePlan_NullProviderPlan_ThrowsBadRequestException( - ChangeProviderPlanCommand command, - SutProvider sutProvider) - { - // Arrange - var providerPlanRepository = sutProvider.GetDependency(); - providerPlanRepository.GetByIdAsync(Arg.Any()).Returns((ProviderPlan)null); - - // Act - var actual = await Assert.ThrowsAsync(() => sutProvider.Sut.ChangePlan(command)); - - // Assert - Assert.Equal("Provider plan not found.", actual.Message); - } - - [Theory, BitAutoData] - public async Task ChangePlan_ProviderNotFound_DoesNothing( - ChangeProviderPlanCommand command, - SutProvider sutProvider) - { - // Arrange - var providerPlanRepository = sutProvider.GetDependency(); - var stripeAdapter = sutProvider.GetDependency(); - var existingPlan = new ProviderPlan - { - Id = command.ProviderPlanId, - PlanType = command.NewPlan, - PurchasedSeats = 0, - AllocatedSeats = 0, - SeatMinimum = 0 - }; - providerPlanRepository - .GetByIdAsync(Arg.Is(p => p == command.ProviderPlanId)) - .Returns(existingPlan); - - // Act - await sutProvider.Sut.ChangePlan(command); - - // Assert - await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any()); - await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any(), Arg.Any()); - } - - [Theory, BitAutoData] - public async Task ChangePlan_SameProviderPlan_DoesNothing( - ChangeProviderPlanCommand command, - SutProvider sutProvider) - { - // Arrange - var providerPlanRepository = sutProvider.GetDependency(); - var stripeAdapter = sutProvider.GetDependency(); - var existingPlan = new ProviderPlan - { - Id = command.ProviderPlanId, - PlanType = command.NewPlan, - PurchasedSeats = 0, - AllocatedSeats = 0, - SeatMinimum = 0 - }; - providerPlanRepository - .GetByIdAsync(Arg.Is(p => p == command.ProviderPlanId)) - .Returns(existingPlan); - - // Act - await sutProvider.Sut.ChangePlan(command); - - // Assert - await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any()); - await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any(), Arg.Any()); - } - - [Theory, BitAutoData] - public async Task ChangePlan_UpdatesSubscriptionCorrectly( - Guid providerPlanId, - Provider provider, - SutProvider sutProvider) - { - // Arrange - var providerPlanRepository = sutProvider.GetDependency(); - var existingPlan = new ProviderPlan - { - Id = providerPlanId, - ProviderId = provider.Id, - PlanType = PlanType.EnterpriseAnnually, - PurchasedSeats = 2, - AllocatedSeats = 10, - SeatMinimum = 8 - }; - providerPlanRepository - .GetByIdAsync(Arg.Is(p => p == providerPlanId)) - .Returns(existingPlan); - - var providerRepository = sutProvider.GetDependency(); - providerRepository.GetByIdAsync(Arg.Is(existingPlan.ProviderId)).Returns(provider); - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter.ProviderSubscriptionGetAsync( - Arg.Is(provider.GatewaySubscriptionId), - Arg.Is(provider.Id)) - .Returns(new Subscription - { - Id = provider.GatewaySubscriptionId, - Items = new StripeList - { - Data = - [ - new SubscriptionItem - { - Id = "si_ent_annual", - Price = new Price - { - Id = StaticStore.GetPlan(PlanType.EnterpriseAnnually).PasswordManager - .StripeProviderPortalSeatPlanId - }, - Quantity = 10 - } - ] - } - }); - - var command = - new ChangeProviderPlanCommand(providerPlanId, PlanType.EnterpriseMonthly, provider.GatewaySubscriptionId); - - // Act - await sutProvider.Sut.ChangePlan(command); - - // Assert - await providerPlanRepository.Received(1) - .ReplaceAsync(Arg.Is(p => p.PlanType == PlanType.EnterpriseMonthly)); - - await stripeAdapter.Received(1) - .SubscriptionUpdateAsync( - Arg.Is(provider.GatewaySubscriptionId), - Arg.Is(p => - p.Items.Count(si => si.Id == "si_ent_annual" && si.Deleted == true) == 1)); - - var newPlanCfg = StaticStore.GetPlan(command.NewPlan); - await stripeAdapter.Received(1) - .SubscriptionUpdateAsync( - Arg.Is(provider.GatewaySubscriptionId), - Arg.Is(p => - p.Items.Count(si => - si.Price == newPlanCfg.PasswordManager.StripeProviderPortalSeatPlanId && - si.Deleted == default && - si.Quantity == 10) == 1)); - } - - #endregion - #region UpdateSeatMinimums [Theory, BitAutoData] @@ -1172,8 +977,6 @@ public async Task UpdateSeatMinimums_NegativeSeatMinimum_ThrowsBadRequestExcepti SutProvider sutProvider) { // Arrange - var providerRepository = sutProvider.GetDependency(); - providerRepository.GetByIdAsync(provider.Id).Returns(provider); var command = new UpdateProviderSeatMinimumsCommand( provider.Id, provider.GatewaySubscriptionId, @@ -1197,7 +1000,6 @@ public async Task UpdateSeatMinimums_NoPurchasedSeats_AllocatedHigherThanIncomin // Arrange var stripeAdapter = sutProvider.GetDependency(); var providerPlanRepository = sutProvider.GetDependency(); - var providerRepository = sutProvider.GetDependency(); const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; @@ -1235,7 +1037,6 @@ public async Task UpdateSeatMinimums_NoPurchasedSeats_AllocatedHigherThanIncomin new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 } }; - providerRepository.GetByIdAsync(provider.Id).Returns(provider); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); var command = new UpdateProviderSeatMinimumsCommand( @@ -1274,8 +1075,6 @@ public async Task UpdateSeatMinimums_NoPurchasedSeats_AllocatedLowerThanIncoming // Arrange var stripeAdapter = sutProvider.GetDependency(); var providerPlanRepository = sutProvider.GetDependency(); - var providerRepository = sutProvider.GetDependency(); - providerRepository.GetByIdAsync(provider.Id).Returns(provider); const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; @@ -1349,8 +1148,6 @@ public async Task UpdateSeatMinimums_PurchasedSeats_NewMinimumLessThanTotal_Upda // Arrange var stripeAdapter = sutProvider.GetDependency(); var providerPlanRepository = sutProvider.GetDependency(); - var providerRepository = sutProvider.GetDependency(); - providerRepository.GetByIdAsync(provider.Id).Returns(provider); const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; @@ -1418,8 +1215,6 @@ public async Task UpdateSeatMinimums_PurchasedSeats_NewMinimumGreaterThanTotal_C // Arrange var stripeAdapter = sutProvider.GetDependency(); var providerPlanRepository = sutProvider.GetDependency(); - var providerRepository = sutProvider.GetDependency(); - providerRepository.GetByIdAsync(provider.Id).Returns(provider); const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; @@ -1493,8 +1288,6 @@ public async Task UpdateSeatMinimums_SinglePlanTypeUpdate_Succeeds( // Arrange var stripeAdapter = sutProvider.GetDependency(); var providerPlanRepository = sutProvider.GetDependency(); - var providerRepository = sutProvider.GetDependency(); - providerRepository.GetByIdAsync(provider.Id).Returns(provider); const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index efab8620c0d7..db41e9282d7e 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -5,8 +5,10 @@ using Bit.Admin.Utilities; using Bit.Core; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Services; using Bit.Core.Context; @@ -230,7 +232,23 @@ public async Task Edit(Guid id) [SelfHosted(NotSelfHostedOnly = true)] public async Task Edit(Guid id, OrganizationEditModel model) { - var organization = await GetOrganization(id, model); + var organization = await _organizationRepository.GetByIdAsync(id); + + if (organization == null) + { + TempData["Error"] = "Could not find organization to update."; + return RedirectToAction("Index"); + } + + var existingOrganizationData = new Organization + { + Id = organization.Id, + Status = organization.Status, + PlanType = organization.PlanType, + Seats = organization.Seats + }; + + UpdateOrganization(organization, model); if (organization.UseSecretsManager && !StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager) @@ -239,7 +257,12 @@ public async Task Edit(Guid id, OrganizationEditModel model) return RedirectToAction("Edit", new { id }); } + await HandlePotentialProviderSeatScalingAsync( + existingOrganizationData, + model); + await _organizationRepository.ReplaceAsync(organization); + await _applicationCacheService.UpsertOrganizationAbilityAsync(organization); await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.OrganizationEditedByAdmin, organization, _currentContext) { @@ -394,10 +417,9 @@ await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider( return Json(null); } - private async Task GetOrganization(Guid id, OrganizationEditModel model) - { - var organization = await _organizationRepository.GetByIdAsync(id); + private void UpdateOrganization(Organization organization, OrganizationEditModel model) + { if (_accessControlService.UserHasPermission(Permission.Org_CheckEnabledBox)) { organization.Enabled = model.Enabled; @@ -449,7 +471,64 @@ private async Task GetOrganization(Guid id, OrganizationEditModel organization.GatewayCustomerId = model.GatewayCustomerId; organization.GatewaySubscriptionId = model.GatewaySubscriptionId; } + } + + private async Task HandlePotentialProviderSeatScalingAsync( + Organization organization, + OrganizationEditModel update) + { + var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); + + var scaleMSPOnClientOrganizationUpdate = + _featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate); + + if (!consolidatedBillingEnabled || !scaleMSPOnClientOrganizationUpdate) + { + return; + } + + var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id); + + // No scaling required + if (provider is not { Type: ProviderType.Msp, Status: ProviderStatusType.Billable } || + organization is not { Status: OrganizationStatusType.Managed } || + !organization.Seats.HasValue || + update is { Seats: null, PlanType: null } || + update is { PlanType: not PlanType.TeamsMonthly and not PlanType.EnterpriseMonthly } || + (PlanTypesMatch() && SeatsMatch())) + { + return; + } + + // Only scale the plan + if (!PlanTypesMatch() && SeatsMatch()) + { + await _providerBillingService.ScaleSeats(provider, organization.PlanType, -organization.Seats.Value); + await _providerBillingService.ScaleSeats(provider, update.PlanType!.Value, organization.Seats.Value); + } + // Only scale the seats + else if (PlanTypesMatch() && !SeatsMatch()) + { + var seatAdjustment = update.Seats!.Value - organization.Seats.Value; + await _providerBillingService.ScaleSeats(provider, organization.PlanType, seatAdjustment); + } + // Scale both + else if (!PlanTypesMatch() && !SeatsMatch()) + { + var seatAdjustment = update.Seats!.Value - organization.Seats.Value; + var planTypeAdjustment = organization.Seats.Value; + var totalAdjustment = seatAdjustment + planTypeAdjustment; + + await _providerBillingService.ScaleSeats(provider, organization.PlanType, -organization.Seats.Value); + await _providerBillingService.ScaleSeats(provider, update.PlanType!.Value, totalAdjustment); + } + + return; + + bool PlanTypesMatch() + => update.PlanType.HasValue && update.PlanType.Value == organization.PlanType; - return organization; + bool SeatsMatch() + => update.Seats.HasValue && update.Seats.Value == organization.Seats; } } diff --git a/src/Api/Billing/Controllers/BaseBillingController.cs b/src/Api/Billing/Controllers/BaseBillingController.cs index 81b8b29f2826..5f7005fdfca2 100644 --- a/src/Api/Billing/Controllers/BaseBillingController.cs +++ b/src/Api/Billing/Controllers/BaseBillingController.cs @@ -22,9 +22,9 @@ public static JsonHttpResult ServerError(string message = "S new ErrorResponseModel(message), statusCode: StatusCodes.Status500InternalServerError); - public static JsonHttpResult Unauthorized() => + public static JsonHttpResult Unauthorized(string message = "Unauthorized.") => TypedResults.Json( - new ErrorResponseModel("Unauthorized."), + new ErrorResponseModel(message), statusCode: StatusCodes.Status401Unauthorized); } } diff --git a/src/Api/Billing/Controllers/ProviderClientsController.cs b/src/Api/Billing/Controllers/ProviderClientsController.cs index 23a6da4590a5..700dd4a2e446 100644 --- a/src/Api/Billing/Controllers/ProviderClientsController.cs +++ b/src/Api/Billing/Controllers/ProviderClientsController.cs @@ -102,15 +102,27 @@ public async Task UpdateAsync( var clientOrganization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId); - if (clientOrganization.Seats != requestBody.AssignedSeats) + if (clientOrganization is not { Status: OrganizationStatusType.Managed }) { - await providerBillingService.AssignSeatsToClientOrganization( - provider, - clientOrganization, - requestBody.AssignedSeats); + return Error.ServerError(); } + var seatAdjustment = requestBody.AssignedSeats - (clientOrganization.Seats ?? 0); + + var seatAdjustmentResultsInPurchase = await providerBillingService.SeatAdjustmentResultsInPurchase( + provider, + clientOrganization.PlanType, + seatAdjustment); + + if (seatAdjustmentResultsInPurchase && !currentContext.ProviderProviderAdmin(provider.Id)) + { + return Error.Unauthorized("Service users cannot purchase additional seats."); + } + + await providerBillingService.ScaleSeats(provider, clientOrganization.PlanType, seatAdjustment); + clientOrganization.Name = requestBody.Name; + clientOrganization.Seats = requestBody.AssignedSeats; await organizationRepository.ReplaceAsync(clientOrganization); diff --git a/src/Core/Billing/Services/IProviderBillingService.cs b/src/Core/Billing/Services/IProviderBillingService.cs index e353e551598c..20e74076282e 100644 --- a/src/Core/Billing/Services/IProviderBillingService.cs +++ b/src/Core/Billing/Services/IProviderBillingService.cs @@ -1,6 +1,5 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Services.Contracts; @@ -12,18 +11,10 @@ namespace Bit.Core.Billing.Services; public interface IProviderBillingService { /// - /// Assigns a specified number of to a client on behalf of - /// its . Seat adjustments for the client organization may autoscale the provider's Stripe - /// depending on the provider's seat minimum for the client 's - /// . + /// Changes the assigned provider plan for the provider. /// - /// The that manages the client . - /// The client whose you want to update. - /// The number of seats to assign to the client organization. - Task AssignSeatsToClientOrganization( - Provider provider, - Organization organization, - int seats); + /// The command to change the provider plan. + Task ChangePlan(ChangeProviderPlanCommand command); /// /// Create a Stripe for the provided client utilizing @@ -44,18 +35,6 @@ Task CreateCustomerForClientOrganization( Task GenerateClientInvoiceReport( string invoiceId); - /// - /// Retrieves the number of seats an MSP has assigned to its client organizations with a specified . - /// - /// The ID of the MSP to retrieve the assigned seat total for. - /// The type of plan to retrieve the assigned seat total for. - /// An representing the number of seats the provider has assigned to its client organizations with the specified . - /// Thrown when the provider represented by the is . - /// Thrown when the provider represented by the has . - Task GetAssignedSeatTotalForPlanOrThrow( - Guid providerId, - PlanType planType); - /// /// 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 @@ -69,6 +48,22 @@ Task ScaleSeats( PlanType planType, int seatAdjustment); + /// + /// Determines whether the provided will result in a purchase for the 's . + /// Seat adjustments that result in purchases include: + /// + /// The going from below the seat minimum to above the seat minimum for the provided + /// The going from above the seat minimum to further above the seat minimum for the provided + /// + /// + /// The provider to check seat adjustments for. + /// The plan type to check seat adjustments for. + /// The change in seats for the 's . + Task SeatAdjustmentResultsInPurchase( + Provider provider, + PlanType planType, + int seatAdjustment); + /// /// For use during the provider setup process, this method creates a Stripe for the specified utilizing the provided . /// @@ -90,12 +85,5 @@ Task SetupCustomer( Task SetupSubscription( Provider provider); - /// - /// Changes the assigned provider plan for the provider. - /// - /// The command to change the provider plan. - /// - Task ChangePlan(ChangeProviderPlanCommand command); - Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command); } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 67d463bac9c5..5830b480eff9 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -156,6 +156,7 @@ 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 PM14401_ScaleMSPOnClientOrganizationUpdate = "PM-14401-scale-msp-on-client-organization-update"; public static List GetAllKeys() { diff --git a/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs new file mode 100644 index 000000000000..af98fef03075 --- /dev/null +++ b/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -0,0 +1,343 @@ +using Bit.Admin.AdminConsole.Controllers; +using Bit.Admin.AdminConsole.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.Enums; +using Bit.Core.Billing.Services; +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; + +namespace Admin.Test.AdminConsole.Controllers; + +[ControllerCustomize(typeof(OrganizationsController))] +[SutProviderCustomize] +public class OrganizationsControllerTests +{ + #region Edit (POST) + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_ProviderSeatScaling_RequiredFFDisabled_NoOp( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + var update = new OrganizationEditModel { UseSecretsManager = false }; + + var organization = new Organization + { + Id = organizationId + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ScaleSeats(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_ProviderSeatScaling_NonBillableProvider_NoOp( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + var update = new OrganizationEditModel { UseSecretsManager = false }; + + var organization = new Organization + { + Id = organizationId + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + var featureService = sutProvider.GetDependency(); + + featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); + featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); + + var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Created }; + + sutProvider.GetDependency().GetByOrganizationIdAsync(organizationId).Returns(provider); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ScaleSeats(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_ProviderSeatScaling_UnmanagedOrganization_NoOp( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + var update = new OrganizationEditModel { UseSecretsManager = false }; + + var organization = new Organization + { + Id = organizationId, + Status = OrganizationStatusType.Created + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + var featureService = sutProvider.GetDependency(); + + featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); + featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); + + var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; + + sutProvider.GetDependency().GetByOrganizationIdAsync(organizationId).Returns(provider); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ScaleSeats(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_ProviderSeatScaling_NonCBPlanType_NoOp( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + + var update = new OrganizationEditModel + { + UseSecretsManager = false, + Seats = 10, + PlanType = PlanType.FamiliesAnnually + }; + + var organization = new Organization + { + Id = organizationId, + Status = OrganizationStatusType.Managed, + Seats = 10 + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + var featureService = sutProvider.GetDependency(); + + featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); + featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); + + var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; + + sutProvider.GetDependency().GetByOrganizationIdAsync(organizationId).Returns(provider); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ScaleSeats(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_ProviderSeatScaling_NoUpdateRequired_NoOp( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + var update = new OrganizationEditModel + { + UseSecretsManager = false, + Seats = 10, + PlanType = PlanType.EnterpriseMonthly + }; + + var organization = new Organization + { + Id = organizationId, + Status = OrganizationStatusType.Managed, + Seats = 10, + PlanType = PlanType.EnterpriseMonthly + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + var featureService = sutProvider.GetDependency(); + + featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); + featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); + + var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; + + sutProvider.GetDependency().GetByOrganizationIdAsync(organizationId).Returns(provider); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ScaleSeats(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_ProviderSeatScaling_PlanTypesUpdate_ScalesSeatsCorrectly( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + var update = new OrganizationEditModel + { + UseSecretsManager = false, + Seats = 10, + PlanType = PlanType.EnterpriseMonthly + }; + + var organization = new Organization + { + Id = organizationId, + Status = OrganizationStatusType.Managed, + Seats = 10, + PlanType = PlanType.TeamsMonthly + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + var featureService = sutProvider.GetDependency(); + + featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); + featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); + + var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; + + sutProvider.GetDependency().GetByOrganizationIdAsync(organizationId).Returns(provider); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + var providerBillingService = sutProvider.GetDependency(); + + await providerBillingService.Received(1).ScaleSeats(provider, organization.PlanType, -organization.Seats.Value); + await providerBillingService.Received(1).ScaleSeats(provider, update.PlanType!.Value, organization.Seats.Value); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_ProviderSeatScaling_SeatsUpdate_ScalesSeatsCorrectly( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + var update = new OrganizationEditModel + { + UseSecretsManager = false, + Seats = 15, + PlanType = PlanType.EnterpriseMonthly + }; + + var organization = new Organization + { + Id = organizationId, + Status = OrganizationStatusType.Managed, + Seats = 10, + PlanType = PlanType.EnterpriseMonthly + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + var featureService = sutProvider.GetDependency(); + + featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); + featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); + + var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; + + sutProvider.GetDependency().GetByOrganizationIdAsync(organizationId).Returns(provider); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + var providerBillingService = sutProvider.GetDependency(); + + await providerBillingService.Received(1).ScaleSeats(provider, organization.PlanType, update.Seats!.Value - organization.Seats.Value); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task Edit_ProviderSeatScaling_FullUpdate_ScalesSeatsCorrectly( + SutProvider sutProvider) + { + // Arrange + var organizationId = new Guid(); + var update = new OrganizationEditModel + { + UseSecretsManager = false, + Seats = 15, + PlanType = PlanType.EnterpriseMonthly + }; + + var organization = new Organization + { + Id = organizationId, + Status = OrganizationStatusType.Managed, + Seats = 10, + PlanType = PlanType.TeamsMonthly + }; + + sutProvider.GetDependency().GetByIdAsync(organizationId) + .Returns(organization); + + var featureService = sutProvider.GetDependency(); + + featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); + featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); + + var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; + + sutProvider.GetDependency().GetByOrganizationIdAsync(organizationId).Returns(provider); + + // Act + _ = await sutProvider.Sut.Edit(organizationId, update); + + // Assert + var providerBillingService = sutProvider.GetDependency(); + + await providerBillingService.Received(1).ScaleSeats(provider, organization.PlanType, -organization.Seats.Value); + await providerBillingService.Received(1).ScaleSeats(provider, update.PlanType!.Value, update.Seats!.Value - organization.Seats.Value + organization.Seats.Value); + } + + #endregion +} diff --git a/test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs b/test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs index 450ff9bf25c5..86bacd9aa3a5 100644 --- a/test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs @@ -5,8 +5,11 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; +using Bit.Core.Billing.Enums; using Bit.Core.Billing.Services; +using Bit.Core.Context; using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Repositories; using Bit.Core.Services; @@ -93,24 +96,7 @@ await sutProvider.GetDependency().Received(1).CreateCus #region UpdateAsync [Theory, BitAutoData] - public async Task UpdateAsync_NoProviderOrganization_NotFound( - Provider provider, - Guid providerOrganizationId, - UpdateClientOrganizationRequestBody requestBody, - SutProvider sutProvider) - { - ConfigureStableProviderServiceUserInputs(provider, sutProvider); - - sutProvider.GetDependency().GetByIdAsync(providerOrganizationId) - .ReturnsNull(); - - var result = await sutProvider.Sut.UpdateAsync(provider.Id, providerOrganizationId, requestBody); - - AssertNotFound(result); - } - - [Theory, BitAutoData] - public async Task UpdateAsync_AssignedSeats_Ok( + public async Task UpdateAsync_ServiceUserMakingPurchase_Unauthorized( Provider provider, Guid providerOrganizationId, UpdateClientOrganizationRequestBody requestBody, @@ -118,6 +104,11 @@ public async Task UpdateAsync_AssignedSeats_Ok( Organization organization, SutProvider sutProvider) { + organization.PlanType = PlanType.TeamsMonthly; + organization.Seats = 10; + organization.Status = OrganizationStatusType.Managed; + requestBody.AssignedSeats = 20; + ConfigureStableProviderServiceUserInputs(provider, sutProvider); sutProvider.GetDependency().GetByIdAsync(providerOrganizationId) @@ -126,22 +117,20 @@ public async Task UpdateAsync_AssignedSeats_Ok( sutProvider.GetDependency().GetByIdAsync(providerOrganization.OrganizationId) .Returns(organization); - var result = await sutProvider.Sut.UpdateAsync(provider.Id, providerOrganizationId, requestBody); + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(false); - await sutProvider.GetDependency().Received(1) - .AssignSeatsToClientOrganization( - provider, - organization, - requestBody.AssignedSeats); + sutProvider.GetDependency().SeatAdjustmentResultsInPurchase( + provider, + PlanType.TeamsMonthly, + 10).Returns(true); - await sutProvider.GetDependency().Received(1) - .ReplaceAsync(Arg.Is(org => org.Name == requestBody.Name)); + var result = await sutProvider.Sut.UpdateAsync(provider.Id, providerOrganizationId, requestBody); - Assert.IsType(result); + AssertUnauthorized(result, message: "Service users cannot purchase additional seats."); } [Theory, BitAutoData] - public async Task UpdateAsync_Name_Ok( + public async Task UpdateAsync_Ok( Provider provider, Guid providerOrganizationId, UpdateClientOrganizationRequestBody requestBody, @@ -149,6 +138,11 @@ public async Task UpdateAsync_Name_Ok( Organization organization, SutProvider sutProvider) { + organization.PlanType = PlanType.TeamsMonthly; + organization.Seats = 10; + organization.Status = OrganizationStatusType.Managed; + requestBody.AssignedSeats = 20; + ConfigureStableProviderServiceUserInputs(provider, sutProvider); sutProvider.GetDependency().GetByIdAsync(providerOrganizationId) @@ -157,18 +151,23 @@ public async Task UpdateAsync_Name_Ok( sutProvider.GetDependency().GetByIdAsync(providerOrganization.OrganizationId) .Returns(organization); - requestBody.AssignedSeats = organization.Seats!.Value; + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(false); + + sutProvider.GetDependency().SeatAdjustmentResultsInPurchase( + provider, + PlanType.TeamsMonthly, + 10).Returns(false); var result = await sutProvider.Sut.UpdateAsync(provider.Id, providerOrganizationId, requestBody); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .AssignSeatsToClientOrganization( - Arg.Any(), - Arg.Any(), - Arg.Any()); + await sutProvider.GetDependency().Received(1) + .ScaleSeats( + provider, + PlanType.TeamsMonthly, + 10); await sutProvider.GetDependency().Received(1) - .ReplaceAsync(Arg.Is(org => org.Name == requestBody.Name)); + .ReplaceAsync(Arg.Is(org => org.Seats == requestBody.AssignedSeats && org.Name == requestBody.Name)); Assert.IsType(result); } diff --git a/test/Api.Test/Billing/Utilities.cs b/test/Api.Test/Billing/Utilities.cs index 36291ec714db..3f5eee72cddf 100644 --- a/test/Api.Test/Billing/Utilities.cs +++ b/test/Api.Test/Billing/Utilities.cs @@ -25,14 +25,14 @@ public static void AssertNotFound(IResult result) Assert.Equal("Resource not found.", response.Message); } - public static void AssertUnauthorized(IResult result) + public static void AssertUnauthorized(IResult result, string message = "Unauthorized.") { Assert.IsType>(result); var response = (JsonHttpResult)result; Assert.Equal(StatusCodes.Status401Unauthorized, response.StatusCode); - Assert.Equal("Unauthorized.", response.Value.Message); + Assert.Equal(message, response.Value.Message); } public static void ConfigureStableProviderAdminInputs( From dfbc400520f4c474b28e1279a86b2fae1da052a9 Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Tue, 12 Nov 2024 15:16:31 -0500 Subject: [PATCH 04/16] Remove FCMv1 feature flag (#5030) * Remove FCMv1 feature flag * Killed a using --- src/Core/Constants.cs | 1 - .../NotificationHubPushRegistrationService.cs | 20 ++++--------------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 5830b480eff9..6978a2c2f68e 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -111,7 +111,6 @@ public static class FeatureFlagKeys public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section"; public const string EmailVerification = "email-verification"; public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays"; - public const string AnhFcmv1Migration = "anh-fcmv1-migration"; public const string ExtensionRefresh = "extension-refresh"; public const string RestrictProviderAccess = "restrict-provider-access"; public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service"; diff --git a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs index ae32babf4477..123152c01c9f 100644 --- a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs @@ -4,7 +4,6 @@ using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.Azure.NotificationHubs; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Bit.Core.NotificationHub; @@ -60,21 +59,10 @@ public async Task CreateOrUpdateRegistrationAsync(string pushToken, string devic switch (type) { case DeviceType.Android: - var featureService = _serviceProvider.GetRequiredService(); - if (featureService.IsEnabled(FeatureFlagKeys.AnhFcmv1Migration)) - { - payloadTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\",\"payload\":\"$(payload)\"}}}"; - messageTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\"}," + - "\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}"; - installation.Platform = NotificationPlatform.FcmV1; - } - else - { - payloadTemplate = "{\"data\":{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}}}"; - messageTemplate = "{\"data\":{\"data\":{\"type\":\"#(type)\"}," + - "\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}"; - installation.Platform = NotificationPlatform.Fcm; - } + payloadTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\",\"payload\":\"$(payload)\"}}}"; + messageTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\"}," + + "\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}"; + installation.Platform = NotificationPlatform.FcmV1; break; case DeviceType.iOS: payloadTemplate = "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}," + From b1776240ea226039b517f8684cbc2cbbf61ea8b3 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:19:16 +0100 Subject: [PATCH 05/16] Pm 14861 vault items fail to load (#5031) * Resolve vault items fail to load * Add hasSubscription to metadata * Remove unused property * Fix the failing unit test --- .../Responses/OrganizationMetadataResponse.cs | 6 ++++-- src/Core/Billing/Models/OrganizationMetadata.cs | 3 ++- .../Implementations/OrganizationBillingService.cs | 15 +++++++++++---- .../OrganizationBillingControllerTests.cs | 3 ++- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs index 960cd53ea2ef..86cbdb92c3d9 100644 --- a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs +++ b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs @@ -6,12 +6,14 @@ public record OrganizationMetadataResponse( bool IsEligibleForSelfHost, bool IsManaged, bool IsOnSecretsManagerStandalone, - bool IsSubscriptionUnpaid) + bool IsSubscriptionUnpaid, + bool HasSubscription) { public static OrganizationMetadataResponse From(OrganizationMetadata metadata) => new( metadata.IsEligibleForSelfHost, metadata.IsManaged, metadata.IsOnSecretsManagerStandalone, - metadata.IsSubscriptionUnpaid); + metadata.IsSubscriptionUnpaid, + metadata.HasSubscription); } diff --git a/src/Core/Billing/Models/OrganizationMetadata.cs b/src/Core/Billing/Models/OrganizationMetadata.cs index 138fb6aef1b9..5bdb450dc653 100644 --- a/src/Core/Billing/Models/OrganizationMetadata.cs +++ b/src/Core/Billing/Models/OrganizationMetadata.cs @@ -4,4 +4,5 @@ public record OrganizationMetadata( bool IsEligibleForSelfHost, bool IsManaged, bool IsOnSecretsManagerStandalone, - bool IsSubscriptionUnpaid); + bool IsSubscriptionUnpaid, + bool HasSubscription); diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index bdcf0fbf6988..eadc5896258f 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -62,18 +62,25 @@ public async Task Finalize(OrganizationSale sale) return null; } + var isEligibleForSelfHost = IsEligibleForSelfHost(organization); + var isManaged = organization.Status == OrganizationStatusType.Managed; + + if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) + { + return new OrganizationMetadata(isEligibleForSelfHost, isManaged, false, + false, false); + } + var customer = await subscriberService.GetCustomer(organization, new CustomerGetOptions { Expand = ["discount.coupon.applies_to"] }); var subscription = await subscriberService.GetSubscription(organization); - - var isEligibleForSelfHost = IsEligibleForSelfHost(organization); - var isManaged = organization.Status == OrganizationStatusType.Managed; var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription); var isSubscriptionUnpaid = IsSubscriptionUnpaid(subscription); + var hasSubscription = true; return new OrganizationMetadata(isEligibleForSelfHost, isManaged, isOnSecretsManagerStandalone, - isSubscriptionUnpaid); + isSubscriptionUnpaid, hasSubscription); } public async Task UpdatePaymentMethod( diff --git a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs index 51e374fd57ab..703475fc57c2 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs @@ -52,7 +52,7 @@ public async Task GetMetadataAsync_OK( { sutProvider.GetDependency().OrganizationUser(organizationId).Returns(true); sutProvider.GetDependency().GetMetadata(organizationId) - .Returns(new OrganizationMetadata(true, true, true, true)); + .Returns(new OrganizationMetadata(true, true, true, true, true)); var result = await sutProvider.Sut.GetMetadataAsync(organizationId); @@ -64,6 +64,7 @@ public async Task GetMetadataAsync_OK( Assert.True(response.IsManaged); Assert.True(response.IsOnSecretsManagerStandalone); Assert.True(response.IsSubscriptionUnpaid); + Assert.True(response.HasSubscription); } [Theory, BitAutoData] From 6f7cdcfcea6ac69e8566699e0f8b9d11b77826b4 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Wed, 13 Nov 2024 15:01:26 +0100 Subject: [PATCH 06/16] [PM-13783] Battle harden ProviderType enum expansion (#5004) Co-authored-by: Matt Bishop --- src/Core/Billing/Extensions/BillingExtensions.cs | 5 ++++- src/Core/Context/CurrentContext.cs | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Core/Billing/Extensions/BillingExtensions.cs b/src/Core/Billing/Extensions/BillingExtensions.cs index 02e8de9244c4..93afbb971dae 100644 --- a/src/Core/Billing/Extensions/BillingExtensions.cs +++ b/src/Core/Billing/Extensions/BillingExtensions.cs @@ -14,7 +14,10 @@ public static bool IsBillable(this Provider provider) => provider.SupportsConsolidatedBilling() && provider.Status == ProviderStatusType.Billable; public static bool SupportsConsolidatedBilling(this Provider provider) - => provider.Type is ProviderType.Msp or ProviderType.MultiOrganizationEnterprise; + => provider.Type.SupportsConsolidatedBilling(); + + public static bool SupportsConsolidatedBilling(this ProviderType providerType) + => providerType is ProviderType.Msp or ProviderType.MultiOrganizationEnterprise; public static bool IsValidClient(this Organization organization) => organization is diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs index e6461575360a..2767b5925fb3 100644 --- a/src/Core/Context/CurrentContext.cs +++ b/src/Core/Context/CurrentContext.cs @@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Extensions; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Identity; @@ -363,9 +364,9 @@ public async Task ManageResetPassword(Guid orgId) public async Task ViewSubscription(Guid orgId) { - var orgManagedByMspProvider = (await GetOrganizationProviderDetails()).Any(po => po.OrganizationId == orgId && po.ProviderType == ProviderType.Msp); + var isManagedByBillableProvider = (await GetOrganizationProviderDetails()).Any(po => po.OrganizationId == orgId && po.ProviderType.SupportsConsolidatedBilling()); - return orgManagedByMspProvider + return isManagedByBillableProvider ? await ProviderUserForOrgAsync(orgId) : await OrganizationOwner(orgId); } From 3d1cd441a76682c7814d4994dc6386fb77744fdf Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:00:29 +0100 Subject: [PATCH 07/16] Remove the flag for upgrade path dialog (#4956) Signed-off-by: Cy Okeke --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 6978a2c2f68e..aed90a89339a 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -125,7 +125,6 @@ public static class FeatureFlagKeys public const string SSHKeyItemVaultItem = "ssh-key-vault-item"; public const string SSHAgent = "ssh-agent"; public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token"; - public const string EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub"; public const string IdpAutoSubmitLogin = "idp-auto-submit-login"; public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor"; From 8b1b07884e9452893a08280d8acc1408f8b3af1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ch=C4=99ci=C5=84ski?= Date: Thu, 14 Nov 2024 13:47:37 +0100 Subject: [PATCH 08/16] Fix github token generating in repository-management.yml workflow (#5038) --- .github/workflows/repository-management.yml | 35 ++++++++++++++------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index 4cbf90c0003f..5cf7a91b0166 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -28,7 +28,6 @@ jobs: runs-on: ubuntu-24.04 outputs: branch: ${{ steps.set-branch.outputs.branch }} - token: ${{ steps.app-token.outputs.token }} steps: - name: Set branch id: set-branch @@ -45,13 +44,6 @@ jobs: echo "branch=$BRANCH" >> $GITHUB_OUTPUT - - name: Generate GH App token - uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 - id: app-token - with: - app-id: ${{ secrets.BW_GHAPP_ID }} - private-key: ${{ secrets.BW_GHAPP_KEY }} - cut_branch: name: Cut branch @@ -59,11 +51,18 @@ jobs: needs: setup runs-on: ubuntu-24.04 steps: + - name: Generate GH App token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + id: app-token + with: + app-id: ${{ secrets.BW_GHAPP_ID }} + private-key: ${{ secrets.BW_GHAPP_KEY }} + - name: Check out target ref uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ inputs.target_ref }} - token: ${{ needs.setup.outputs.token }} + token: ${{ steps.app-token.outputs.token }} - name: Check if ${{ needs.setup.outputs.branch }} branch exists env: @@ -98,11 +97,18 @@ jobs: with: version: ${{ inputs.version_number_override }} + - name: Generate GH App token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + id: app-token + with: + app-id: ${{ secrets.BW_GHAPP_ID }} + private-key: ${{ secrets.BW_GHAPP_KEY }} + - name: Check out branch uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: main - token: ${{ needs.setup.outputs.token }} + token: ${{ steps.app-token.outputs.token }} - name: Configure Git run: | @@ -190,11 +196,18 @@ jobs: - bump_version - setup steps: + - name: Generate GH App token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + id: app-token + with: + app-id: ${{ secrets.BW_GHAPP_ID }} + private-key: ${{ secrets.BW_GHAPP_KEY }} + - name: Check out main branch uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: main - token: ${{ needs.setup.outputs.token }} + token: ${{ steps.app-token.outputs.token }} - name: Configure Git run: | From eee7494c916feea5a881140888c0cec1cff4861b Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Thu, 14 Nov 2024 14:54:20 -0800 Subject: [PATCH 09/16] [PM-14373] Introduce SecurityTask database table and repository (#5025) * [PM-14373] Introduce SecurityTask entity and related enums * [PM-14373] Add Dapper SecurityTask repository * [PM-14373] Introduce MSSQL table, view, and stored procedures * [PM-14373] Add EF SecurityTask repository and type configurations * [PM-14373] Add EF Migration * [PM-14373] Add integration tests * [PM-14373] Formatting * Typo Co-authored-by: Matt Bishop * Typo Co-authored-by: Matt Bishop * [PM-14373] Remove DeleteById sproc * [PM-14373] SQL formatting --------- Co-authored-by: Matt Bishop --- src/Core/Vault/Entities/SecurityTask.cs | 20 + src/Core/Vault/Enums/SecurityTaskStatus.cs | 18 + src/Core/Vault/Enums/SecurityTaskType.cs | 12 + .../Repositories/ISecurityTaskRepository.cs | 9 + .../DapperServiceCollectionExtensions.cs | 1 + .../Repositories/SecurityTaskRepository.cs | 18 + ...ityFrameworkServiceCollectionExtensions.cs | 1 + .../Repositories/DatabaseContext.cs | 1 + .../SecurityTaskEntityTypeConfiguration.cs | 30 + .../Vault/Models/SecurityTask.cs | 18 + .../Repositories/SecurityTaskRepository.cs | 14 + .../SecurityTask/SecurityTask_Create.sql | 33 + .../SecurityTask/SecurityTask_ReadById.sql | 13 + .../SecurityTask/SecurityTask_Update.sql | 24 + src/Sql/Vault/dbo/Tables/SecurityTask.sql | 21 + src/Sql/Vault/dbo/Views/SecurityTaskView.sql | 6 + .../SecurityTaskRepositoryTests.cs | 123 + .../2024-11-11_00_InitialSecurityTasks.sql | 114 + .../20241112001902_SecurityTasks.Designer.cs | 2940 ++++++++++++++++ .../20241112001902_SecurityTasks.cs | 59 + .../DatabaseContextModelSnapshot.cs | 52 + .../20241112001757_SecurityTasks.Designer.cs | 2946 +++++++++++++++++ .../20241112001757_SecurityTasks.cs | 58 + .../DatabaseContextModelSnapshot.cs | 52 + .../20241112001814_SecurityTasks.Designer.cs | 2929 ++++++++++++++++ .../20241112001814_SecurityTasks.cs | 58 + .../DatabaseContextModelSnapshot.cs | 52 + 27 files changed, 9622 insertions(+) create mode 100644 src/Core/Vault/Entities/SecurityTask.cs create mode 100644 src/Core/Vault/Enums/SecurityTaskStatus.cs create mode 100644 src/Core/Vault/Enums/SecurityTaskType.cs create mode 100644 src/Core/Vault/Repositories/ISecurityTaskRepository.cs create mode 100644 src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs create mode 100644 src/Infrastructure.EntityFramework/Vault/Configurations/SecurityTaskEntityTypeConfiguration.cs create mode 100644 src/Infrastructure.EntityFramework/Vault/Models/SecurityTask.cs create mode 100644 src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs create mode 100644 src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_Create.sql create mode 100644 src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadById.sql create mode 100644 src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_Update.sql create mode 100644 src/Sql/Vault/dbo/Tables/SecurityTask.sql create mode 100644 src/Sql/Vault/dbo/Views/SecurityTaskView.sql create mode 100644 test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs create mode 100644 util/Migrator/DbScripts/2024-11-11_00_InitialSecurityTasks.sql create mode 100644 util/MySqlMigrations/Migrations/20241112001902_SecurityTasks.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20241112001902_SecurityTasks.cs create mode 100644 util/PostgresMigrations/Migrations/20241112001757_SecurityTasks.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20241112001757_SecurityTasks.cs create mode 100644 util/SqliteMigrations/Migrations/20241112001814_SecurityTasks.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20241112001814_SecurityTasks.cs diff --git a/src/Core/Vault/Entities/SecurityTask.cs b/src/Core/Vault/Entities/SecurityTask.cs new file mode 100644 index 000000000000..926c5f908dd4 --- /dev/null +++ b/src/Core/Vault/Entities/SecurityTask.cs @@ -0,0 +1,20 @@ +using Bit.Core.Entities; +using Bit.Core.Utilities; + +namespace Bit.Core.Vault.Entities; + +public class SecurityTask : ITableObject +{ + public Guid Id { get; set; } + public Guid OrganizationId { get; set; } + public Guid? CipherId { get; set; } + public Enums.SecurityTaskType Type { get; set; } + public Enums.SecurityTaskStatus Status { get; set; } + public DateTime CreationDate { get; set; } = DateTime.UtcNow; + public DateTime RevisionDate { get; set; } = DateTime.UtcNow; + + public void SetNewId() + { + Id = CoreHelpers.GenerateComb(); + } +} diff --git a/src/Core/Vault/Enums/SecurityTaskStatus.cs b/src/Core/Vault/Enums/SecurityTaskStatus.cs new file mode 100644 index 000000000000..4dd0a65a5317 --- /dev/null +++ b/src/Core/Vault/Enums/SecurityTaskStatus.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Vault.Enums; + +public enum SecurityTaskStatus : byte +{ + /// + /// Default status for newly created tasks that have not been completed. + /// + [Display(Name = "Pending")] + Pending = 0, + + /// + /// Status when a task is considered complete and has no remaining actions + /// + [Display(Name = "Completed")] + Completed = 1, +} diff --git a/src/Core/Vault/Enums/SecurityTaskType.cs b/src/Core/Vault/Enums/SecurityTaskType.cs new file mode 100644 index 000000000000..eb8884e58e21 --- /dev/null +++ b/src/Core/Vault/Enums/SecurityTaskType.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Vault.Enums; + +public enum SecurityTaskType : byte +{ + /// + /// Task to update a cipher's password that was found to be at-risk by an administrator + /// + [Display(Name = "Update at-risk credential")] + UpdateAtRiskCredential = 0 +} diff --git a/src/Core/Vault/Repositories/ISecurityTaskRepository.cs b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs new file mode 100644 index 000000000000..f2262f207acb --- /dev/null +++ b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs @@ -0,0 +1,9 @@ +using Bit.Core.Repositories; +using Bit.Core.Vault.Entities; + +namespace Bit.Core.Vault.Repositories; + +public interface ISecurityTaskRepository : IRepository +{ + +} diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index 77528dc80496..550c572cf509 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -59,6 +59,7 @@ public static void AddDapperRepositories(this IServiceCollection services, bool services .AddSingleton(); services.AddSingleton(); + services.AddSingleton(); if (selfHosted) { diff --git a/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs new file mode 100644 index 000000000000..1674b965f010 --- /dev/null +++ b/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs @@ -0,0 +1,18 @@ +using Bit.Core.Settings; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Repositories; +using Bit.Infrastructure.Dapper.Repositories; + +namespace Bit.Infrastructure.Dapper.Vault.Repositories; + +public class SecurityTaskRepository : Repository, ISecurityTaskRepository +{ + public SecurityTaskRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { } + + public SecurityTaskRepository(string connectionString, string readOnlyConnectionString) + : base(connectionString, readOnlyConnectionString) + { } + +} diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index 0e33895bf62a..b8c84f649f21 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -96,6 +96,7 @@ public static void AddPasswordManagerEFRepositories(this IServiceCollection serv services .AddSingleton(); services.AddSingleton(); + services.AddSingleton(); if (selfHosted) { diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs index 97f004c1f791..1f1ea16bfcae 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs @@ -77,6 +77,7 @@ public DatabaseContext(DbContextOptions options) public DbSet NotificationStatuses { get; set; } public DbSet ClientOrganizationMigrationRecords { get; set; } public DbSet PasswordHealthReportApplications { get; set; } + public DbSet SecurityTasks { get; set; } protected override void OnModelCreating(ModelBuilder builder) { diff --git a/src/Infrastructure.EntityFramework/Vault/Configurations/SecurityTaskEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/Vault/Configurations/SecurityTaskEntityTypeConfiguration.cs new file mode 100644 index 000000000000..5ad111b08891 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Vault/Configurations/SecurityTaskEntityTypeConfiguration.cs @@ -0,0 +1,30 @@ +using Bit.Infrastructure.EntityFramework.Vault.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Bit.Infrastructure.EntityFramework.Vault.Configurations; + +public class SecurityTaskEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .Property(s => s.Id) + .ValueGeneratedNever(); + + builder + .HasKey(s => s.Id) + .IsClustered(); + + builder + .HasIndex(s => s.OrganizationId) + .IsClustered(false); + + builder + .HasIndex(s => s.CipherId) + .IsClustered(false); + + builder + .ToTable(nameof(SecurityTask)); + } +} diff --git a/src/Infrastructure.EntityFramework/Vault/Models/SecurityTask.cs b/src/Infrastructure.EntityFramework/Vault/Models/SecurityTask.cs new file mode 100644 index 000000000000..828c3bbc7d33 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Vault/Models/SecurityTask.cs @@ -0,0 +1,18 @@ +using AutoMapper; +using Bit.Infrastructure.EntityFramework.AdminConsole.Models; + +namespace Bit.Infrastructure.EntityFramework.Vault.Models; + +public class SecurityTask : Core.Vault.Entities.SecurityTask +{ + public virtual Organization Organization { get; set; } + public virtual Cipher Cipher { get; set; } +} + +public class SecurityTaskMapperProfile : Profile +{ + public SecurityTaskMapperProfile() + { + CreateMap().ReverseMap(); + } +} diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs new file mode 100644 index 000000000000..82c06bcc6b20 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs @@ -0,0 +1,14 @@ +using AutoMapper; +using Bit.Core.Vault.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.Vault.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Infrastructure.EntityFramework.Vault.Repositories; + +public class SecurityTaskRepository : Repository, ISecurityTaskRepository +{ + public SecurityTaskRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) + : base(serviceScopeFactory, mapper, (context) => context.SecurityTasks) + { } +} diff --git a/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_Create.sql b/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_Create.sql new file mode 100644 index 000000000000..0d44b52435d4 --- /dev/null +++ b/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_Create.sql @@ -0,0 +1,33 @@ +CREATE PROCEDURE [dbo].[SecurityTask_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @Type TINYINT, + @Status TINYINT, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[SecurityTask] + ( + [Id], + [OrganizationId], + [CipherId], + [Type], + [Status], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @OrganizationId, + @CipherId, + @Type, + @Status, + @CreationDate, + @RevisionDate + ) +END diff --git a/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadById.sql b/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadById.sql new file mode 100644 index 000000000000..a25079a62c2e --- /dev/null +++ b/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadById.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[SecurityTask_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[SecurityTaskView] + WHERE + [Id] = @Id +END diff --git a/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_Update.sql b/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_Update.sql new file mode 100644 index 000000000000..390662d8393f --- /dev/null +++ b/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_Update.sql @@ -0,0 +1,24 @@ +CREATE PROCEDURE [dbo].[SecurityTask_Update] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @Type TINYINT, + @Status TINYINT, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[SecurityTask] + SET + [OrganizationId] = @OrganizationId, + [CipherId] = @CipherId, + [Type] = @Type, + [Status] = @Status, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id +END diff --git a/src/Sql/Vault/dbo/Tables/SecurityTask.sql b/src/Sql/Vault/dbo/Tables/SecurityTask.sql new file mode 100644 index 000000000000..a00dcede9c6f --- /dev/null +++ b/src/Sql/Vault/dbo/Tables/SecurityTask.sql @@ -0,0 +1,21 @@ +CREATE TABLE [dbo].[SecurityTask] +( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [CipherId] UNIQUEIDENTIFIER NULL, + [Type] TINYINT NOT NULL, + [Status] TINYINT NOT NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [RevisionDate] DATETIME2 (7) NOT NULL, + CONSTRAINT [PK_SecurityTask] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_SecurityTask_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE, + CONSTRAINT [FK_SecurityTask_Cipher] FOREIGN KEY ([CipherId]) REFERENCES [dbo].[Cipher] ([Id]) ON DELETE CASCADE, +); + +GO +CREATE NONCLUSTERED INDEX [IX_SecurityTask_CipherId] + ON [dbo].[SecurityTask]([CipherId] ASC) WHERE CipherId IS NOT NULL; + +GO +CREATE NONCLUSTERED INDEX [IX_SecurityTask_OrganizationId] + ON [dbo].[SecurityTask]([OrganizationId] ASC) WHERE OrganizationId IS NOT NULL; diff --git a/src/Sql/Vault/dbo/Views/SecurityTaskView.sql b/src/Sql/Vault/dbo/Views/SecurityTaskView.sql new file mode 100644 index 000000000000..b4140a4a8b93 --- /dev/null +++ b/src/Sql/Vault/dbo/Views/SecurityTaskView.sql @@ -0,0 +1,6 @@ +CREATE VIEW [dbo].[SecurityTaskView] +AS +SELECT + * +FROM + [dbo].[SecurityTask] diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs new file mode 100644 index 000000000000..79cc1d2bc9b7 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs @@ -0,0 +1,123 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Enums; +using Bit.Core.Repositories; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; +using Bit.Core.Vault.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.Vault.Repositories; + +public class SecurityTaskRepositoryTests +{ + [DatabaseTheory, DatabaseData] + public async Task CreateAsync( + IOrganizationRepository organizationRepository, + ICipherRepository cipherRepository, + ISecurityTaskRepository securityTaskRepository) + { + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + PlanType = PlanType.EnterpriseAnnually, + Plan = "Test Plan", + BillingEmail = "billing@email.com" + }); + + var cipher = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "", + }); + + var task = await securityTaskRepository.CreateAsync(new SecurityTask + { + OrganizationId = organization.Id, + CipherId = cipher.Id, + Status = SecurityTaskStatus.Pending, + Type = SecurityTaskType.UpdateAtRiskCredential, + }); + + Assert.NotNull(task); + } + + [DatabaseTheory, DatabaseData] + public async Task ReadByIdAsync( + IOrganizationRepository organizationRepository, + ICipherRepository cipherRepository, + ISecurityTaskRepository securityTaskRepository) + { + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + PlanType = PlanType.EnterpriseAnnually, + Plan = "Test Plan", + BillingEmail = "billing@email.com" + }); + + var cipher = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "", + }); + + var task = await securityTaskRepository.CreateAsync(new SecurityTask + { + OrganizationId = organization.Id, + CipherId = cipher.Id, + Status = SecurityTaskStatus.Pending, + Type = SecurityTaskType.UpdateAtRiskCredential, + }); + + Assert.NotNull(task); + + var readTask = await securityTaskRepository.GetByIdAsync(task.Id); + + Assert.NotNull(readTask); + Assert.Equal(task.Id, readTask.Id); + Assert.Equal(task.Status, readTask.Status); + } + + [DatabaseTheory, DatabaseData] + public async Task UpdateAsync( + IOrganizationRepository organizationRepository, + ICipherRepository cipherRepository, + ISecurityTaskRepository securityTaskRepository) + { + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + PlanType = PlanType.EnterpriseAnnually, + Plan = "Test Plan", + BillingEmail = "billing@email.com" + }); + + var cipher = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "", + }); + + var task = await securityTaskRepository.CreateAsync(new SecurityTask + { + OrganizationId = organization.Id, + CipherId = cipher.Id, + Status = SecurityTaskStatus.Pending, + Type = SecurityTaskType.UpdateAtRiskCredential, + }); + + Assert.NotNull(task); + + task.Status = SecurityTaskStatus.Completed; + await securityTaskRepository.ReplaceAsync(task); + + var updatedTask = await securityTaskRepository.GetByIdAsync(task.Id); + + Assert.NotNull(updatedTask); + Assert.Equal(task.Id, updatedTask.Id); + Assert.Equal(SecurityTaskStatus.Completed, updatedTask.Status); + } +} diff --git a/util/Migrator/DbScripts/2024-11-11_00_InitialSecurityTasks.sql b/util/Migrator/DbScripts/2024-11-11_00_InitialSecurityTasks.sql new file mode 100644 index 000000000000..b39e8c8f9d34 --- /dev/null +++ b/util/Migrator/DbScripts/2024-11-11_00_InitialSecurityTasks.sql @@ -0,0 +1,114 @@ +-- Security Tasks + +-- Table +IF OBJECT_ID('[dbo].[SecurityTask]') IS NULL + BEGIN + CREATE TABLE [dbo].[SecurityTask] + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [CipherId] UNIQUEIDENTIFIER NULL, + [Type] TINYINT NOT NULL, + [Status] TINYINT NOT NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [RevisionDate] DATETIME2 (7) NOT NULL, + CONSTRAINT [PK_SecurityTask] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_SecurityTask_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE, + CONSTRAINT [FK_SecurityTask_Cipher] FOREIGN KEY ([CipherId]) REFERENCES [dbo].[Cipher] ([Id]) ON DELETE CASCADE, + ); + + CREATE NONCLUSTERED INDEX [IX_SecurityTask_CipherId] + ON [dbo].[SecurityTask]([CipherId] ASC) WHERE CipherId IS NOT NULL; + + CREATE NONCLUSTERED INDEX [IX_SecurityTask_OrganizationId] + ON [dbo].[SecurityTask]([OrganizationId] ASC) WHERE OrganizationId IS NOT NULL; + END +GO + +-- View SecurityTask +CREATE OR ALTER VIEW [dbo].[SecurityTaskView] +AS +SELECT + * +FROM + [dbo].[SecurityTask] +GO + +-- Stored Procedures: Create +CREATE OR ALTER PROCEDURE [dbo].[SecurityTask_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @Type TINYINT, + @Status TINYINT, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[SecurityTask] + ( + [Id], + [OrganizationId], + [CipherId], + [Type], + [Status], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @OrganizationId, + @CipherId, + @Type, + @Status, + @CreationDate, + @RevisionDate + ) +END +GO + +-- Stored Procedures: Update +CREATE OR ALTER PROCEDURE [dbo].[SecurityTask_Update] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @Type TINYINT, + @Status TINYINT, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[SecurityTask] + SET + [OrganizationId] = @OrganizationId, + [CipherId] = @CipherId, + [Type] = @Type, + [Status] = @Status, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id +END +GO + +-- Stored Procedures: ReadById +CREATE OR ALTER PROCEDURE [dbo].[SecurityTask_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[SecurityTaskView] + WHERE + [Id] = @Id +END +GO diff --git a/util/MySqlMigrations/Migrations/20241112001902_SecurityTasks.Designer.cs b/util/MySqlMigrations/Migrations/20241112001902_SecurityTasks.Designer.cs new file mode 100644 index 000000000000..03053e4744df --- /dev/null +++ b/util/MySqlMigrations/Migrations/20241112001902_SecurityTasks.Designer.cs @@ -0,0 +1,2940 @@ +// +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("20241112001902_SecurityTasks")] + partial class SecurityTasks + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionCreationDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasColumnType("longtext"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AaGuid") + .HasColumnType("char(36)"); + + b.Property("Counter") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasColumnType("longtext"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20241112001902_SecurityTasks.cs b/util/MySqlMigrations/Migrations/20241112001902_SecurityTasks.cs new file mode 100644 index 000000000000..85f417c312bd --- /dev/null +++ b/util/MySqlMigrations/Migrations/20241112001902_SecurityTasks.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class SecurityTasks : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SecurityTask", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + OrganizationId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + CipherId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + Type = table.Column(type: "tinyint unsigned", nullable: false), + Status = table.Column(type: "tinyint unsigned", nullable: false), + CreationDate = table.Column(type: "datetime(6)", nullable: false), + RevisionDate = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SecurityTask", x => x.Id); + table.ForeignKey( + name: "FK_SecurityTask_Cipher_CipherId", + column: x => x.CipherId, + principalTable: "Cipher", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_SecurityTask_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_SecurityTask_CipherId", + table: "SecurityTask", + column: "CipherId"); + + migrationBuilder.CreateIndex( + name: "IX_SecurityTask_OrganizationId", + table: "SecurityTask", + column: "OrganizationId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SecurityTask"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 23792e47e5f7..36c46f629f60 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1989,6 +1989,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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") @@ -2643,6 +2678,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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) diff --git a/util/PostgresMigrations/Migrations/20241112001757_SecurityTasks.Designer.cs b/util/PostgresMigrations/Migrations/20241112001757_SecurityTasks.Designer.cs new file mode 100644 index 000000000000..7e81e6715e55 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20241112001757_SecurityTasks.Designer.cs @@ -0,0 +1,2946 @@ +// +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("20241112001757_SecurityTasks")] + partial class SecurityTasks + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionCreationDeletion") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasColumnType("text"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MasterPasswordHash") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("OrganizationId", "ExternalId"), new[] { "UserId" }); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("Counter") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsPrf") + .HasColumnType("boolean"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasColumnType("text"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20241112001757_SecurityTasks.cs b/util/PostgresMigrations/Migrations/20241112001757_SecurityTasks.cs new file mode 100644 index 000000000000..479b1f6c55c4 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20241112001757_SecurityTasks.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class SecurityTasks : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SecurityTask", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrganizationId = table.Column(type: "uuid", nullable: false), + CipherId = table.Column(type: "uuid", nullable: true), + Type = table.Column(type: "smallint", nullable: false), + Status = table.Column(type: "smallint", nullable: false), + CreationDate = table.Column(type: "timestamp with time zone", nullable: false), + RevisionDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SecurityTask", x => x.Id); + table.ForeignKey( + name: "FK_SecurityTask_Cipher_CipherId", + column: x => x.CipherId, + principalTable: "Cipher", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_SecurityTask_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_SecurityTask_CipherId", + table: "SecurityTask", + column: "CipherId"); + + migrationBuilder.CreateIndex( + name: "IX_SecurityTask_OrganizationId", + table: "SecurityTask", + column: "OrganizationId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SecurityTask"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index e344e7663a5e..69c9dae16088 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1995,6 +1995,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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") @@ -2649,6 +2684,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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) diff --git a/util/SqliteMigrations/Migrations/20241112001814_SecurityTasks.Designer.cs b/util/SqliteMigrations/Migrations/20241112001814_SecurityTasks.Designer.cs new file mode 100644 index 000000000000..b9f09be7cd65 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20241112001814_SecurityTasks.Designer.cs @@ -0,0 +1,2929 @@ +// +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("20241112001814_SecurityTasks")] + partial class SecurityTasks + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionCreationDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResponseDate") + .HasColumnType("TEXT"); + + b.Property("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GranteeId") + .HasColumnType("TEXT"); + + b.Property("GrantorId") + .HasColumnType("TEXT"); + + b.Property("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WaitTimeDays") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AaGuid") + .HasColumnType("TEXT"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/SqliteMigrations/Migrations/20241112001814_SecurityTasks.cs b/util/SqliteMigrations/Migrations/20241112001814_SecurityTasks.cs new file mode 100644 index 000000000000..cf9c74dfe673 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20241112001814_SecurityTasks.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class SecurityTasks : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SecurityTask", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + CipherId = table.Column(type: "TEXT", nullable: true), + Type = table.Column(type: "INTEGER", nullable: false), + Status = table.Column(type: "INTEGER", nullable: false), + CreationDate = table.Column(type: "TEXT", nullable: false), + RevisionDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SecurityTask", x => x.Id); + table.ForeignKey( + name: "FK_SecurityTask_Cipher_CipherId", + column: x => x.CipherId, + principalTable: "Cipher", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_SecurityTask_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_SecurityTask_CipherId", + table: "SecurityTask", + column: "CipherId"); + + migrationBuilder.CreateIndex( + name: "IX_SecurityTask_OrganizationId", + table: "SecurityTask", + column: "OrganizationId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SecurityTask"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 9d7902abffec..67390bcbcb40 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1978,6 +1978,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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") @@ -2632,6 +2667,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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) From df21d574e1c252fa0b4eb8a07492945b109fad0d Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:30:03 -0500 Subject: [PATCH 10/16] [PM-11798] Remove `enable-consolidated-billing` feature flag (#5028) * Remove flag from CreateProviderCommand * Remove flag from OrganizationsController * Consolidate provider extensions * Remove flag from ProvidersController * Remove flag from CreateMsp.cshtml * Remove flag from Provider Edit.cshtml Also ensured the editable Gateway fields show for Multi-organization enterprises * Remove flag from OrganizationsController * Remove flag from billing-owned provider controllers * Remove flag from OrganizationService * Remove flag from RemoveOrganizationFromProviderCommand * Remove flag from ProviderService * Remove flag * Run dotnet format * Fix failing tests --- .../Providers/CreateProviderCommand.cs | 33 ++------ .../RemoveOrganizationFromProviderCommand.cs | 11 +-- .../AdminConsole/Services/ProviderService.cs | 68 ++++++--------- ...oveOrganizationFromProviderCommandTests.cs | 7 -- .../Services/ProviderServiceTests.cs | 68 ++++----------- .../Controllers/OrganizationsController.cs | 16 ++-- .../Controllers/ProvidersController.cs | 9 +- .../Views/Providers/CreateMsp.cshtml | 8 -- .../AdminConsole/Views/Providers/Edit.cshtml | 82 +++++++++---------- .../Controllers/OrganizationsController.cs | 7 +- .../Controllers/BaseProviderController.cs | 13 +-- .../Controllers/ProviderBillingController.cs | 3 +- .../Controllers/ProviderClientsController.cs | 3 +- .../Implementations/OrganizationService.cs | 13 +-- .../Billing/Extensions/BillingExtensions.cs | 18 ++-- src/Core/Constants.cs | 1 - .../OrganizationsControllerTests.cs | 7 -- .../OrganizationsControllerTests.cs | 2 - .../ProviderBillingControllerTests.cs | 45 ---------- test/Api.Test/Billing/Utilities.cs | 5 -- .../Services/OrganizationServiceTests.cs | 2 - 21 files changed, 119 insertions(+), 302 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/CreateProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/CreateProviderCommand.cs index 3b01370ef70b..69b7e67ec1c9 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/CreateProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/CreateProviderCommand.cs @@ -1,5 +1,4 @@ -using Bit.Core; -using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; @@ -10,7 +9,6 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; -using Bit.Core.Services; namespace Bit.Commercial.Core.AdminConsole.Providers; @@ -21,35 +19,28 @@ public class CreateProviderCommand : ICreateProviderCommand private readonly IProviderService _providerService; private readonly IUserRepository _userRepository; private readonly IProviderPlanRepository _providerPlanRepository; - private readonly IFeatureService _featureService; public CreateProviderCommand( IProviderRepository providerRepository, IProviderUserRepository providerUserRepository, IProviderService providerService, IUserRepository userRepository, - IProviderPlanRepository providerPlanRepository, - IFeatureService featureService) + IProviderPlanRepository providerPlanRepository) { _providerRepository = providerRepository; _providerUserRepository = providerUserRepository; _providerService = providerService; _userRepository = userRepository; _providerPlanRepository = providerPlanRepository; - _featureService = featureService; } public async Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats) { var providerId = await CreateProviderAsync(provider, ownerEmail); - var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); - - if (isConsolidatedBillingEnabled) - { - await CreateProviderPlanAsync(providerId, PlanType.TeamsMonthly, teamsMinimumSeats); - await CreateProviderPlanAsync(providerId, PlanType.EnterpriseMonthly, enterpriseMinimumSeats); - } + await Task.WhenAll( + CreateProviderPlanAsync(providerId, PlanType.TeamsMonthly, teamsMinimumSeats), + CreateProviderPlanAsync(providerId, PlanType.EnterpriseMonthly, enterpriseMinimumSeats)); } public async Task CreateResellerAsync(Provider provider) @@ -61,12 +52,7 @@ public async Task CreateMultiOrganizationEnterpriseAsync(Provider provider, stri { var providerId = await CreateProviderAsync(provider, ownerEmail); - var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); - - if (isConsolidatedBillingEnabled) - { - await CreateProviderPlanAsync(providerId, plan, minimumSeats); - } + await CreateProviderPlanAsync(providerId, plan, minimumSeats); } private async Task CreateProviderAsync(Provider provider, string ownerEmail) @@ -77,12 +63,7 @@ private async Task CreateProviderAsync(Provider provider, string ownerEmai throw new BadRequestException("Invalid owner. Owner must be an existing Bitwarden user."); } - var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); - - if (isConsolidatedBillingEnabled) - { - provider.Gateway = GatewayType.Stripe; - } + provider.Gateway = GatewayType.Stripe; await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Pending); diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs index 045fd505928d..ce0c0c93357d 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs @@ -1,7 +1,5 @@ -using Bit.Core; -using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; @@ -102,11 +100,8 @@ private async Task ResetOrganizationBillingAsync( Provider provider, IEnumerable organizationOwnerEmails) { - var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); - - if (isConsolidatedBillingEnabled && - provider.Status == ProviderStatusType.Billable && - organization.Status == OrganizationStatusType.Managed && + if (provider.IsBillable() && + organization.IsValidClient() && !string.IsNullOrEmpty(organization.GatewayCustomerId)) { await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs index 48ea903adaf3..e384d71df904 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs @@ -8,7 +8,6 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; @@ -101,24 +100,16 @@ public async Task CompleteSetupAsync(Provider provider, Guid ownerUser throw new BadRequestException("Invalid owner."); } - if (!_featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)) + if (taxInfo == null || string.IsNullOrEmpty(taxInfo.BillingAddressCountry) || string.IsNullOrEmpty(taxInfo.BillingAddressPostalCode)) { - provider.Status = ProviderStatusType.Created; - await _providerRepository.UpsertAsync(provider); - } - else - { - if (taxInfo == null || string.IsNullOrEmpty(taxInfo.BillingAddressCountry) || string.IsNullOrEmpty(taxInfo.BillingAddressPostalCode)) - { - throw new BadRequestException("Both address and postal code are required to set up your provider."); - } - var customer = await _providerBillingService.SetupCustomer(provider, taxInfo); - provider.GatewayCustomerId = customer.Id; - var subscription = await _providerBillingService.SetupSubscription(provider); - provider.GatewaySubscriptionId = subscription.Id; - provider.Status = ProviderStatusType.Billable; - await _providerRepository.UpsertAsync(provider); + throw new BadRequestException("Both address and postal code are required to set up your provider."); } + var customer = await _providerBillingService.SetupCustomer(provider, taxInfo); + provider.GatewayCustomerId = customer.Id; + var subscription = await _providerBillingService.SetupSubscription(provider); + provider.GatewaySubscriptionId = subscription.Id; + provider.Status = ProviderStatusType.Billable; + await _providerRepository.UpsertAsync(provider); providerUser.Key = key; await _providerUserRepository.ReplaceAsync(providerUser); @@ -545,13 +536,9 @@ public async Task CreateOrganizationAsync(Guid providerId, { var provider = await _providerRepository.GetByIdAsync(providerId); - var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && provider.IsBillable(); + ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan); - ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan, consolidatedBillingEnabled); - - var (organization, _, defaultCollection) = consolidatedBillingEnabled - ? await _organizationService.SignupClientAsync(organizationSignup) - : await _organizationService.SignUpAsync(organizationSignup); + var (organization, _, defaultCollection) = await _organizationService.SignupClientAsync(organizationSignup); var providerOrganization = new ProviderOrganization { @@ -687,27 +674,24 @@ private async Task HasConfirmedProviderAdminExceptAsync(Guid providerId, I return confirmedOwnersIds.Except(providerUserIds).Any(); } - private void ThrowOnInvalidPlanType(ProviderType providerType, PlanType requestedType, bool consolidatedBillingEnabled = false) + private void ThrowOnInvalidPlanType(ProviderType providerType, PlanType requestedType) { - if (consolidatedBillingEnabled) + switch (providerType) { - switch (providerType) - { - case ProviderType.Msp: - if (requestedType is not (PlanType.TeamsMonthly or PlanType.EnterpriseMonthly)) - { - throw new BadRequestException($"Managed Service Providers cannot manage organizations with the plan type {requestedType}. Only Teams (Monthly) and Enterprise (Monthly) are allowed."); - } - break; - case ProviderType.MultiOrganizationEnterprise: - if (requestedType is not (PlanType.EnterpriseMonthly or PlanType.EnterpriseAnnually)) - { - throw new BadRequestException($"Multi-organization Enterprise Providers cannot manage organizations with the plan type {requestedType}. Only Enterprise (Monthly) and Enterprise (Annually) are allowed."); - } - break; - default: - throw new BadRequestException($"Unsupported provider type {providerType}."); - } + case ProviderType.Msp: + if (requestedType is not (PlanType.TeamsMonthly or PlanType.EnterpriseMonthly)) + { + throw new BadRequestException($"Managed Service Providers cannot manage organizations with the plan type {requestedType}. Only Teams (Monthly) and Enterprise (Monthly) are allowed."); + } + break; + case ProviderType.MultiOrganizationEnterprise: + if (requestedType is not (PlanType.EnterpriseMonthly or PlanType.EnterpriseAnnually)) + { + throw new BadRequestException($"Multi-organization Enterprise Providers cannot manage organizations with the plan type {requestedType}. Only Enterprise (Monthly) and Enterprise (Annually) are allowed."); + } + break; + default: + throw new BadRequestException($"Unsupported provider type {providerType}."); } if (ProviderDisallowedOrganizationTypes.Contains(requestedType)) diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs index e984259e95e9..f45ab75046e5 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs @@ -1,5 +1,4 @@ using Bit.Commercial.Core.AdminConsole.Providers; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; @@ -155,9 +154,6 @@ public async Task RemoveOrganizationFromProvider_OrganizationStripeEnabled_NonCo "b@example.com" ]); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) - .Returns(false); - sutProvider.GetDependency().SubscriptionGetAsync(organization.GatewaySubscriptionId) .Returns(GetSubscription(organization.GatewaySubscriptionId)); @@ -222,9 +218,6 @@ public async Task RemoveOrganizationFromProvider_OrganizationStripeEnabled_Conso "b@example.com" ]); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) - .Returns(true); - var stripeAdapter = sutProvider.GetDependency(); stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(new Subscription diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs index 4aac363b9c78..2883c9d7e384 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs @@ -1,6 +1,5 @@ using Bit.Commercial.Core.AdminConsole.Services; using Bit.Commercial.Core.Test.AdminConsole.AutoFixture; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; @@ -55,36 +54,8 @@ public async Task CompleteSetupAsync_TokenIsInvalid_Throws(User user, Provider p } [Theory, BitAutoData] - public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, - [ProviderUser(ProviderUserStatusType.Confirmed, ProviderUserType.ProviderAdmin)] ProviderUser providerUser, - SutProvider sutProvider) - { - providerUser.ProviderId = provider.Id; - providerUser.UserId = user.Id; - var userService = sutProvider.GetDependency(); - userService.GetUserByIdAsync(user.Id).Returns(user); - - var providerUserRepository = sutProvider.GetDependency(); - providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser); - - var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName"); - var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector"); - sutProvider.GetDependency().CreateProtector("ProviderServiceDataProtector") - .Returns(protector); - sutProvider.Create(); - - var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); - - await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key); - - await sutProvider.GetDependency().Received().UpsertAsync(provider); - await sutProvider.GetDependency().Received() - .ReplaceAsync(Arg.Is(pu => pu.UserId == user.Id && pu.ProviderId == provider.Id && pu.Key == key)); - } - - [Theory, BitAutoData] - public async Task CompleteSetupAsync_ConsolidatedBilling_Success(User user, Provider provider, string key, TaxInfo taxInfo, - [ProviderUser(ProviderUserStatusType.Confirmed, ProviderUserType.ProviderAdmin)] ProviderUser providerUser, + public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TaxInfo taxInfo, + [ProviderUser] ProviderUser providerUser, SutProvider sutProvider) { providerUser.ProviderId = provider.Id; @@ -100,9 +71,6 @@ public async Task CompleteSetupAsync_ConsolidatedBilling_Success(User user, Prov sutProvider.GetDependency().CreateProtector("ProviderServiceDataProtector") .Returns(protector); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) - .Returns(true); - var providerBillingService = sutProvider.GetDependency(); var customer = new Customer { Id = "customer_id" }; @@ -489,7 +457,7 @@ public async Task AddOrganization_OrganizationAlreadyBelongsToAProvider_Throws(P public async Task AddOrganization_OrganizationHasSecretsManager_Throws(Provider provider, Organization organization, string key, SutProvider sutProvider) { - organization.PlanType = PlanType.EnterpriseAnnually; + organization.PlanType = PlanType.EnterpriseMonthly; organization.UseSecretsManager = true; sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); @@ -506,7 +474,7 @@ public async Task AddOrganization_OrganizationHasSecretsManager_Throws(Provider public async Task AddOrganization_Success(Provider provider, Organization organization, string key, SutProvider sutProvider) { - organization.PlanType = PlanType.EnterpriseAnnually; + organization.PlanType = PlanType.EnterpriseMonthly; var providerRepository = sutProvider.GetDependency(); providerRepository.GetByIdAsync(provider.Id).Returns(provider); @@ -549,8 +517,8 @@ public async Task AddOrganization_CreateAfterNov62023_PlanTypeDoesNotUpdated(Pro sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); var providerOrganizationRepository = sutProvider.GetDependency(); - var expectedPlanType = PlanType.EnterpriseAnnually; - organization.PlanType = PlanType.EnterpriseAnnually; + var expectedPlanType = PlanType.EnterpriseMonthly; + organization.PlanType = PlanType.EnterpriseMonthly; sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key); @@ -579,12 +547,12 @@ public async Task AddOrganization_CreateBeforeNov62023_PlanTypeUpdated(Provider BackdateProviderCreationDate(provider, newCreationDate); provider.Type = ProviderType.Msp; - organization.PlanType = PlanType.EnterpriseAnnually; - organization.Plan = "Enterprise (Annually)"; + organization.PlanType = PlanType.EnterpriseMonthly; + organization.Plan = "Enterprise (Monthly)"; - var expectedPlanType = PlanType.EnterpriseAnnually2020; + var expectedPlanType = PlanType.EnterpriseMonthly2020; - var expectedPlanId = "2020-enterprise-org-seat-annually"; + var expectedPlanId = "2020-enterprise-org-seat-monthly"; sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); var providerOrganizationRepository = sutProvider.GetDependency(); @@ -663,11 +631,11 @@ public async Task AddOrganizationsToReseller_WithMspProvider_Throws(Provider pro public async Task CreateOrganizationAsync_Success(Provider provider, OrganizationSignup organizationSignup, Organization organization, string clientOwnerEmail, User user, SutProvider sutProvider) { - organizationSignup.Plan = PlanType.EnterpriseAnnually; + organizationSignup.Plan = PlanType.EnterpriseMonthly; sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); var providerOrganizationRepository = sutProvider.GetDependency(); - sutProvider.GetDependency().SignUpAsync(organizationSignup) + sutProvider.GetDependency().SignupClientAsync(organizationSignup) .Returns((organization, null as OrganizationUser, new Collection())); var providerOrganization = @@ -688,7 +656,7 @@ await sutProvider.GetDependency() } [Theory, OrganizationCustomize, BitAutoData] - public async Task CreateOrganizationAsync_ConsolidatedBillingEnabled_InvalidPlanType_ThrowsBadRequestException( + public async Task CreateOrganizationAsync_InvalidPlanType_ThrowsBadRequestException( Provider provider, OrganizationSignup organizationSignup, Organization organization, @@ -696,8 +664,6 @@ public async Task CreateOrganizationAsync_ConsolidatedBillingEnabled_InvalidPlan User user, SutProvider sutProvider) { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); - provider.Type = ProviderType.Msp; provider.Status = ProviderStatusType.Billable; @@ -717,7 +683,7 @@ await Assert.ThrowsAsync(() => } [Theory, OrganizationCustomize, BitAutoData] - public async Task CreateOrganizationAsync_ConsolidatedBillingEnabled_InvokeSignupClientAsync( + public async Task CreateOrganizationAsync_InvokeSignupClientAsync( Provider provider, OrganizationSignup organizationSignup, Organization organization, @@ -725,8 +691,6 @@ public async Task CreateOrganizationAsync_ConsolidatedBillingEnabled_InvokeSignu User user, SutProvider sutProvider) { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); - provider.Type = ProviderType.Msp; provider.Status = ProviderStatusType.Billable; @@ -771,11 +735,11 @@ public async Task CreateOrganizationAsync_SetsAccessAllToFalse (Provider provider, OrganizationSignup organizationSignup, Organization organization, string clientOwnerEmail, User user, SutProvider sutProvider, Collection defaultCollection) { - organizationSignup.Plan = PlanType.EnterpriseAnnually; + organizationSignup.Plan = PlanType.EnterpriseMonthly; sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); var providerOrganizationRepository = sutProvider.GetDependency(); - sutProvider.GetDependency().SignUpAsync(organizationSignup) + sutProvider.GetDependency().SignupClientAsync(organizationSignup) .Returns((organization, null as OrganizationUser, defaultCollection)); var providerOrganization = diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index db41e9282d7e..67a80a3754c1 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -55,8 +55,8 @@ public class OrganizationsController : Controller private readonly IServiceAccountRepository _serviceAccountRepository; private readonly IProviderOrganizationRepository _providerOrganizationRepository; private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand; - private readonly IFeatureService _featureService; private readonly IProviderBillingService _providerBillingService; + private readonly IFeatureService _featureService; public OrganizationsController( IOrganizationService organizationService, @@ -82,8 +82,8 @@ public OrganizationsController( IServiceAccountRepository serviceAccountRepository, IProviderOrganizationRepository providerOrganizationRepository, IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand, - IFeatureService featureService, - IProviderBillingService providerBillingService) + IProviderBillingService providerBillingService, + IFeatureService featureService) { _organizationService = organizationService; _organizationRepository = organizationRepository; @@ -108,8 +108,8 @@ public OrganizationsController( _serviceAccountRepository = serviceAccountRepository; _providerOrganizationRepository = providerOrganizationRepository; _removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand; - _featureService = featureService; _providerBillingService = providerBillingService; + _featureService = featureService; } [RequirePermission(Permission.Org_List_View)] @@ -285,9 +285,7 @@ public async Task Delete(Guid id) return RedirectToAction("Index"); } - var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); - - if (consolidatedBillingEnabled && organization.IsValidClient()) + if (organization.IsValidClient()) { var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id); @@ -477,12 +475,10 @@ private async Task HandlePotentialProviderSeatScalingAsync( Organization organization, OrganizationEditModel update) { - var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); - var scaleMSPOnClientOrganizationUpdate = _featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate); - if (!consolidatedBillingEnabled || !scaleMSPOnClientOrganizationUpdate) + if (!scaleMSPOnClientOrganizationUpdate) { return; } diff --git a/src/Admin/AdminConsole/Controllers/ProvidersController.cs b/src/Admin/AdminConsole/Controllers/ProvidersController.cs index 83e4ce7d5127..8a56483a605f 100644 --- a/src/Admin/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Admin/AdminConsole/Controllers/ProvidersController.cs @@ -282,9 +282,7 @@ public async Task Edit(Guid id, ProviderEditModel model) await _providerRepository.ReplaceAsync(provider); await _applicationCacheService.UpsertProviderAbilityAsync(provider); - var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); - - if (!isConsolidatedBillingEnabled || !provider.IsBillable()) + if (!provider.IsBillable()) { return RedirectToAction("Edit", new { id }); } @@ -340,10 +338,7 @@ private async Task GetEditModel(Guid id) var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id); var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id); - var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); - - - if (!isConsolidatedBillingEnabled || !provider.IsBillable()) + if (!provider.IsBillable()) { return new ProviderEditModel(provider, users, providerOrganizations, new List()); } diff --git a/src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml b/src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml index dde62b58a9fa..d28348196fa7 100644 --- a/src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml @@ -1,10 +1,5 @@ -@using Bit.Core.AdminConsole.Enums.Provider -@using Bit.Core - @model CreateMspProviderModel -@inject Bit.Core.Services.IFeatureService FeatureService - @{ ViewData["Title"] = "Create Managed Service Provider"; } @@ -17,8 +12,6 @@ - @if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)) - {
@@ -33,7 +26,6 @@
- } diff --git a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml index 53944d0fc31f..005c498aa2a5 100644 --- a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml @@ -48,7 +48,7 @@ - @if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable()) + @if (Model.Provider.IsBillable()) { switch (Model.Provider.Type) { @@ -68,46 +68,6 @@ -
-
-
-
- - -
-
-
-
-
-
-
- -
- -
- - - -
-
-
-
-
-
- -
- -
- - - -
-
-
-
-
break; } case ProviderType.MultiOrganizationEnterprise: @@ -141,6 +101,46 @@ break; } } +
+
+
+
+ + +
+
+
+
+
+
+
+ +
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ + + +
+
+
+
+
} @await Html.PartialAsync("Organizations", Model) diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index e134adc042c1..88734a6c770a 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -289,9 +289,7 @@ public async Task Delete(string id, [FromBody] SecretVerificationRequestModel mo throw new BadRequestException(string.Empty, "User verification failed."); } - var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); - - if (consolidatedBillingEnabled && organization.IsValidClient()) + if (organization.IsValidClient()) { var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id); @@ -322,8 +320,7 @@ public async Task PostDeleteRecoverToken(Guid id, [FromBody] OrganizationVerifyD throw new BadRequestException("Invalid token."); } - var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); - if (consolidatedBillingEnabled && organization.IsValidClient()) + if (organization.IsValidClient()) { var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id); if (provider.IsBillable()) diff --git a/src/Api/Billing/Controllers/BaseProviderController.cs b/src/Api/Billing/Controllers/BaseProviderController.cs index ecc9f23a70ab..038abfaa9eaf 100644 --- a/src/Api/Billing/Controllers/BaseProviderController.cs +++ b/src/Api/Billing/Controllers/BaseProviderController.cs @@ -1,5 +1,4 @@ -using Bit.Core; -using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Extensions; using Bit.Core.Context; @@ -9,7 +8,6 @@ namespace Bit.Api.Billing.Controllers; public abstract class BaseProviderController( ICurrentContext currentContext, - IFeatureService featureService, ILogger logger, IProviderRepository providerRepository, IUserService userService) : BaseBillingController @@ -26,15 +24,6 @@ public abstract class BaseProviderController( Guid providerId, Func checkAuthorization) { - if (!featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)) - { - logger.LogError( - "Cannot run Consolidated Billing operation for provider ({ProviderID}) while feature flag is disabled", - providerId); - - return (null, Error.NotFound()); - } - var provider = await providerRepository.GetByIdAsync(providerId); if (provider == null) diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index d2209685aa75..f7ddf0853e20 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -19,14 +19,13 @@ namespace Bit.Api.Billing.Controllers; [Authorize("Application")] public class ProviderBillingController( ICurrentContext currentContext, - IFeatureService featureService, ILogger logger, IProviderBillingService providerBillingService, IProviderPlanRepository providerPlanRepository, IProviderRepository providerRepository, ISubscriberService subscriberService, IStripeAdapter stripeAdapter, - IUserService userService) : BaseProviderController(currentContext, featureService, logger, providerRepository, userService) + IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService) { [HttpGet("invoices")] public async Task GetInvoicesAsync([FromRoute] Guid providerId) diff --git a/src/Api/Billing/Controllers/ProviderClientsController.cs b/src/Api/Billing/Controllers/ProviderClientsController.cs index 700dd4a2e446..0c09fa7bafcf 100644 --- a/src/Api/Billing/Controllers/ProviderClientsController.cs +++ b/src/Api/Billing/Controllers/ProviderClientsController.cs @@ -14,14 +14,13 @@ namespace Bit.Api.Billing.Controllers; [Route("providers/{providerId:guid}/clients")] public class ProviderClientsController( ICurrentContext currentContext, - IFeatureService featureService, ILogger logger, IOrganizationRepository organizationRepository, IProviderBillingService providerBillingService, IProviderOrganizationRepository providerOrganizationRepository, IProviderRepository providerRepository, IProviderService providerService, - IUserService userService) : BaseProviderController(currentContext, featureService, logger, providerRepository, userService) + IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService) { [HttpPost] public async Task CreateAsync( diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index f44ce686f4ed..47c79aa13e8a 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -15,6 +15,7 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Services; using Bit.Core.Context; @@ -444,13 +445,6 @@ public async Task VerifyBankAsync(Guid organizationId, int amount1, int amount2) public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup) { - var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); - - if (!consolidatedBillingEnabled) - { - throw new InvalidOperationException($"{nameof(SignupClientAsync)} is only for use within Consolidated Billing"); - } - var plan = StaticStore.GetPlan(signup.Plan); ValidatePlan(plan, signup.AdditionalSeats, "Password Manager"); @@ -1443,10 +1437,7 @@ public async Task>> ConfirmUsersAsync(Guid if (provider is { Enabled: true }) { - var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); - - if (consolidatedBillingEnabled && provider.Type == ProviderType.Msp && - provider.Status == ProviderStatusType.Billable) + if (provider.IsBillable()) { return (false, "Seat limit has been reached. Please contact your provider to add more seats."); } diff --git a/src/Core/Billing/Extensions/BillingExtensions.cs b/src/Core/Billing/Extensions/BillingExtensions.cs index 93afbb971dae..39b92e95a248 100644 --- a/src/Core/Billing/Extensions/BillingExtensions.cs +++ b/src/Core/Billing/Extensions/BillingExtensions.cs @@ -11,10 +11,11 @@ namespace Bit.Core.Billing.Extensions; public static class BillingExtensions { public static bool IsBillable(this Provider provider) => - provider.SupportsConsolidatedBilling() && provider.Status == ProviderStatusType.Billable; - - public static bool SupportsConsolidatedBilling(this Provider provider) - => provider.Type.SupportsConsolidatedBilling(); + provider is + { + Type: ProviderType.Msp or ProviderType.MultiOrganizationEnterprise, + Status: ProviderStatusType.Billable + }; public static bool SupportsConsolidatedBilling(this ProviderType providerType) => providerType is ProviderType.Msp or ProviderType.MultiOrganizationEnterprise; @@ -24,12 +25,15 @@ public static bool IsValidClient(this Organization organization) { Seats: not null, Status: OrganizationStatusType.Managed, - PlanType: PlanType.TeamsMonthly or PlanType.EnterpriseMonthly + PlanType: PlanType.TeamsMonthly or PlanType.EnterpriseMonthly or PlanType.EnterpriseAnnually }; public static bool IsStripeEnabled(this ISubscriber subscriber) - => !string.IsNullOrEmpty(subscriber.GatewayCustomerId) && - !string.IsNullOrEmpty(subscriber.GatewaySubscriptionId); + => subscriber is + { + GatewayCustomerId: not null and not "", + GatewaySubscriptionId: not null and not "" + }; public static bool IsUnverifiedBankAccount(this SetupIntent setupIntent) => setupIntent is diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index aed90a89339a..3e40eb7a1601 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -107,7 +107,6 @@ public static class FeatureFlagKeys 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 EnableConsolidatedBilling = "enable-consolidated-billing"; public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section"; public const string EmailVerification = "email-verification"; public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays"; diff --git a/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index af98fef03075..485126ebb2cc 100644 --- a/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -68,7 +68,6 @@ public async Task Edit_ProviderSeatScaling_NonBillableProvider_NoOp( var featureService = sutProvider.GetDependency(); - featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Created }; @@ -104,7 +103,6 @@ public async Task Edit_ProviderSeatScaling_UnmanagedOrganization_NoOp( var featureService = sutProvider.GetDependency(); - featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; @@ -147,7 +145,6 @@ public async Task Edit_ProviderSeatScaling_NonCBPlanType_NoOp( var featureService = sutProvider.GetDependency(); - featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; @@ -190,7 +187,6 @@ public async Task Edit_ProviderSeatScaling_NoUpdateRequired_NoOp( var featureService = sutProvider.GetDependency(); - featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; @@ -233,7 +229,6 @@ public async Task Edit_ProviderSeatScaling_PlanTypesUpdate_ScalesSeatsCorrectly( var featureService = sutProvider.GetDependency(); - featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; @@ -278,7 +273,6 @@ public async Task Edit_ProviderSeatScaling_SeatsUpdate_ScalesSeatsCorrectly( var featureService = sutProvider.GetDependency(); - featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; @@ -322,7 +316,6 @@ public async Task Edit_ProviderSeatScaling_FullUpdate_ScalesSeatsCorrectly( var featureService = sutProvider.GetDependency(); - featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true); var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable }; diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 13826888d797..27c0f7a7c334 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -218,8 +218,6 @@ public async Task Delete_OrganizationIsConsolidatedBillingClient_ScalesProviders _userService.VerifySecretAsync(user, requestModel.Secret).Returns(true); - _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); - _providerRepository.GetByOrganizationIdAsync(organization.Id).Returns(provider); await _sut.Delete(organizationId.ToString(), requestModel); diff --git a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs index 73e3850c89e7..d46038ae90cd 100644 --- a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs @@ -1,7 +1,6 @@ using Bit.Api.Billing.Controllers; using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; -using Bit.Core; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; @@ -35,27 +34,11 @@ public class ProviderBillingControllerTests { #region GetInvoicesAsync & TryGetBillableProviderForAdminOperations - [Theory, BitAutoData] - public async Task GetInvoicesAsync_FFDisabled_NotFound( - Guid providerId, - SutProvider sutProvider) - { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) - .Returns(false); - - var result = await sutProvider.Sut.GetInvoicesAsync(providerId); - - AssertNotFound(result); - } - [Theory, BitAutoData] public async Task GetInvoicesAsync_NullProvider_NotFound( Guid providerId, SutProvider sutProvider) { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) - .Returns(true); - sutProvider.GetDependency().GetByIdAsync(providerId).ReturnsNull(); var result = await sutProvider.Sut.GetInvoicesAsync(providerId); @@ -68,9 +51,6 @@ public async Task GetInvoicesAsync_NotProviderUser_Unauthorized( Provider provider, SutProvider sutProvider) { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) - .Returns(true); - sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); sutProvider.GetDependency().ProviderProviderAdmin(provider.Id) @@ -86,9 +66,6 @@ public async Task GetInvoicesAsync_ProviderNotBillable_Unauthorized( Provider provider, SutProvider sutProvider) { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) - .Returns(true); - provider.Type = ProviderType.Reseller; provider.Status = ProviderStatusType.Created; @@ -229,27 +206,11 @@ public async Task GenerateClientInvoiceReportAsync_Ok( #region GetSubscriptionAsync & TryGetBillableProviderForServiceUserOperation - [Theory, BitAutoData] - public async Task GetSubscriptionAsync_FFDisabled_NotFound( - Guid providerId, - SutProvider sutProvider) - { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) - .Returns(false); - - var result = await sutProvider.Sut.GetSubscriptionAsync(providerId); - - AssertNotFound(result); - } - [Theory, BitAutoData] public async Task GetSubscriptionAsync_NullProvider_NotFound( Guid providerId, SutProvider sutProvider) { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) - .Returns(true); - sutProvider.GetDependency().GetByIdAsync(providerId).ReturnsNull(); var result = await sutProvider.Sut.GetSubscriptionAsync(providerId); @@ -262,9 +223,6 @@ public async Task GetSubscriptionAsync_NotProviderUser_Unauthorized( Provider provider, SutProvider sutProvider) { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) - .Returns(true); - sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); sutProvider.GetDependency().ProviderUser(provider.Id) @@ -280,9 +238,6 @@ public async Task GetSubscriptionAsync_ProviderNotBillable_Unauthorized( Provider provider, SutProvider sutProvider) { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) - .Returns(true); - provider.Type = ProviderType.Reseller; provider.Status = ProviderStatusType.Created; diff --git a/test/Api.Test/Billing/Utilities.cs b/test/Api.Test/Billing/Utilities.cs index 3f5eee72cddf..fd79c71ca215 100644 --- a/test/Api.Test/Billing/Utilities.cs +++ b/test/Api.Test/Billing/Utilities.cs @@ -1,11 +1,9 @@ using Bit.Api.Billing.Controllers; -using Bit.Core; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Context; using Bit.Core.Models.Api; -using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; @@ -59,9 +57,6 @@ private static void ConfigureBaseProviderInputs( Provider provider, SutProvider sutProvider) where T : BaseProviderController { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) - .Returns(true); - provider.Type = ProviderType.Msp; provider.Status = ProviderStatusType.Billable; diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index 147162c66610..e09293f32d66 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -420,8 +420,6 @@ public async Task SignupClientAsync_Succeeds( OrganizationSignup signup, SutProvider sutProvider) { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); - signup.Plan = PlanType.TeamsMonthly; var (organization, _, _) = await sutProvider.Sut.SignupClientAsync(signup); From e16cad50b16a8bed011f86c039e6892c4ebfe9cf Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:54:23 -0500 Subject: [PATCH 11/16] Add Teams to SCIM API key generation (#5036) --- src/Api/AdminConsole/Controllers/OrganizationsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 88734a6c770a..0ac750e66502 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -354,7 +354,7 @@ public async Task ApiKey(string id, [FromBody] Organization { // Non-enterprise orgs should not be able to create or view an apikey of billing sync/scim key types var plan = StaticStore.GetPlan(organization.PlanType); - if (plan.ProductTier != ProductTierType.Enterprise) + if (plan.ProductTier is not ProductTierType.Enterprise and not ProductTierType.Teams) { throw new NotFoundException(); } From ab5d4738d6a2b08cf8c75db4dd2a1a2c36656fb5 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:58:05 -0800 Subject: [PATCH 12/16] [PM-8107] Remove Duo v2 from server (#4934) refactor(TwoFactorAuthentication): Remove references to old Duo SDK version 2 code and replace them with the Duo SDK version 4 supported library DuoUniversal code. Increased unit test coverage in the Two Factor Authentication code space. We opted to use DI instead of Inheritance for the Duo and OrganizaitonDuo two factor tokens to increase testability, since creating a testing mock of the Duo.Client was non-trivial. Reviewed-by: @JaredSnider-Bitwarden --- .../Auth/Controllers/TwoFactorController.cs | 44 +-- .../Models/Request/TwoFactorRequestModels.cs | 63 ++-- .../TwoFactor/TwoFactorDuoResponseModel.cs | 71 +---- src/Api/Startup.cs | 3 +- src/Core/Auth/Identity/DuoWebTokenProvider.cs | 86 ----- .../OrganizationDuoWebTokenProvider.cs | 76 ----- .../Identity/TemporaryDuoWebV4SDKService.cs | 172 ---------- .../AuthenticatorTokenProvider.cs | 2 +- .../DuoUniversalTokenProvider.cs | 102 ++++++ .../DuoUniversalTokenService.cs | 177 +++++++++++ .../EmailTokenProvider.cs | 2 +- .../EmailTwoFactorTokenProvider.cs | 2 +- .../IOrganizationTwoFactorTokenProvider.cs | 2 +- .../OrganizationDuoUniversalTokenProvider.cs | 81 +++++ .../TwoFactorRememberTokenProvider.cs | 2 +- .../WebAuthnTokenProvider.cs | 2 +- .../YubicoOtpTokenProvider.cs | 6 +- src/Core/Auth/Utilities/DuoApi.cs | 277 ---------------- src/Core/Auth/Utilities/DuoWeb.cs | 240 -------------- .../TwoFactorAuthenticationValidator.cs | 48 +-- .../Utilities/ServiceCollectionExtensions.cs | 7 +- .../Controllers/TwoFactorControllerTests.cs | 295 ++++++++++++++++++ ...ganizationTwoFactorDuoRequestModelTests.cs | 60 ---- ...TwoFactorDuoRequestModelValidationTests.cs | 9 +- .../UserTwoFactorDuoRequestModelTests.cs | 62 ---- ...anizationTwoFactorDuoResponseModelTests.cs | 71 +---- .../UserTwoFactorDuoResponseModelTests.cs | 68 ++-- .../Attributes/BitCustomizeAttribute.cs | 4 +- .../AuthenticationTokenProviderTests.cs | 2 +- .../Auth/Identity/BaseTokenProviderTests.cs | 5 +- ...DuoUniversalTwoFactorTokenProviderTests.cs | 262 ++++++++++++++++ .../EmailTwoFactorTokenProviderTests.cs | 2 +- ...DuoUniversalTwoFactorTokenProviderTests.cs | 289 +++++++++++++++++ .../Services/DuoUniversalTokenServiceTests.cs | 91 ++++++ .../Endpoints/IdentityServerTwoFactorTests.cs | 13 +- .../TwoFactorAuthenticationValidatorTests.cs | 81 +---- 36 files changed, 1411 insertions(+), 1368 deletions(-) delete mode 100644 src/Core/Auth/Identity/DuoWebTokenProvider.cs delete mode 100644 src/Core/Auth/Identity/OrganizationDuoWebTokenProvider.cs delete mode 100644 src/Core/Auth/Identity/TemporaryDuoWebV4SDKService.cs rename src/Core/Auth/Identity/{ => TokenProviders}/AuthenticatorTokenProvider.cs (97%) create mode 100644 src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs create mode 100644 src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenService.cs rename src/Core/Auth/Identity/{ => TokenProviders}/EmailTokenProvider.cs (97%) rename src/Core/Auth/Identity/{ => TokenProviders}/EmailTwoFactorTokenProvider.cs (97%) rename src/Core/Auth/Identity/{ => TokenProviders}/IOrganizationTwoFactorTokenProvider.cs (87%) create mode 100644 src/Core/Auth/Identity/TokenProviders/OrganizationDuoUniversalTokenProvider.cs rename src/Core/Auth/Identity/{ => TokenProviders}/TwoFactorRememberTokenProvider.cs (92%) rename src/Core/Auth/Identity/{ => TokenProviders}/WebAuthnTokenProvider.cs (99%) rename src/Core/Auth/Identity/{ => TokenProviders}/YubicoOtpTokenProvider.cs (93%) delete mode 100644 src/Core/Auth/Utilities/DuoApi.cs delete mode 100644 src/Core/Auth/Utilities/DuoWeb.cs create mode 100644 test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs create mode 100644 test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs create mode 100644 test/Core.Test/Auth/Identity/OrganizationDuoUniversalTwoFactorTokenProviderTests.cs create mode 100644 test/Core.Test/Auth/Services/DuoUniversalTokenServiceTests.cs diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index f2578fbc6551..a00a64313dfd 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -4,15 +4,14 @@ using Bit.Api.Models.Request; using Bit.Api.Models.Response; using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces; using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Auth.Utilities; 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.Tokens; using Bit.Core.Utilities; using Fido2NetLib; @@ -29,11 +28,10 @@ public class TwoFactorController : Controller private readonly IUserService _userService; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationService _organizationService; - private readonly GlobalSettings _globalSettings; private readonly UserManager _userManager; private readonly ICurrentContext _currentContext; private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand; - private readonly IFeatureService _featureService; + private readonly IDuoUniversalTokenService _duoUniversalTokenService; private readonly IDataProtectorTokenFactory _twoFactorAuthenticatorDataProtector; private readonly IDataProtectorTokenFactory _ssoEmailTwoFactorSessionDataProtector; @@ -41,22 +39,20 @@ public TwoFactorController( IUserService userService, IOrganizationRepository organizationRepository, IOrganizationService organizationService, - GlobalSettings globalSettings, UserManager userManager, ICurrentContext currentContext, IVerifyAuthRequestCommand verifyAuthRequestCommand, - IFeatureService featureService, + IDuoUniversalTokenService duoUniversalConfigService, IDataProtectorTokenFactory twoFactorAuthenticatorDataProtector, IDataProtectorTokenFactory ssoEmailTwoFactorSessionDataProtector) { _userService = userService; _organizationRepository = organizationRepository; _organizationService = organizationService; - _globalSettings = globalSettings; _userManager = userManager; _currentContext = currentContext; _verifyAuthRequestCommand = verifyAuthRequestCommand; - _featureService = featureService; + _duoUniversalTokenService = duoUniversalConfigService; _twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector; _ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector; } @@ -184,21 +180,7 @@ public async Task GetDuo([FromBody] SecretVerificatio public async Task PutDuo([FromBody] UpdateTwoFactorDuoRequestModel model) { var user = await CheckAsync(model, true); - try - { - // for backwards compatibility - will be removed with PM-8107 - DuoApi duoApi = null; - if (model.ClientId != null && model.ClientSecret != null) - { - duoApi = new DuoApi(model.ClientId, model.ClientSecret, model.Host); - } - else - { - duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host); - } - await duoApi.JSONApiCall("GET", "/auth/v2/check"); - } - catch (DuoException) + if (!await _duoUniversalTokenService.ValidateDuoConfiguration(model.ClientSecret, model.ClientId, model.Host)) { throw new BadRequestException( "Duo configuration settings are not valid. Please re-check the Duo Admin panel."); @@ -241,21 +223,7 @@ public async Task PutOrganizationDuo(string id, } var organization = await _organizationRepository.GetByIdAsync(orgIdGuid) ?? throw new NotFoundException(); - try - { - // for backwards compatibility - will be removed with PM-8107 - DuoApi duoApi = null; - if (model.ClientId != null && model.ClientSecret != null) - { - duoApi = new DuoApi(model.ClientId, model.ClientSecret, model.Host); - } - else - { - duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host); - } - await duoApi.JSONApiCall("GET", "/auth/v2/check"); - } - catch (DuoException) + if (!await _duoUniversalTokenService.ValidateDuoConfiguration(model.ClientId, model.ClientSecret, model.Host)) { throw new BadRequestException( "Duo configuration settings are not valid. Please re-check the Duo Admin panel."); diff --git a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs index f2f01a2378e5..357db5ad1e11 100644 --- a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs +++ b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs @@ -2,8 +2,8 @@ using Bit.Api.Auth.Models.Request.Accounts; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Models; -using Bit.Core.Auth.Utilities; using Bit.Core.Entities; using Fido2NetLib; @@ -43,21 +43,16 @@ public User ToUser(User existingUser) public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IValidatableObject { /* - To support both v2 and v4 we need to remove the required annotation from the properties. - todo - the required annotation will be added back in PM-8107. + String lengths based on Duo's documentation + https://github.com/duosecurity/duo_universal_csharp/blob/main/DuoUniversal/Client.cs */ - [StringLength(50)] + [Required] + [StringLength(20, MinimumLength = 20, ErrorMessage = "Client Id must be exactly 20 characters.")] public string ClientId { get; set; } - [StringLength(50)] + [Required] + [StringLength(40, MinimumLength = 40, ErrorMessage = "Client Secret must be exactly 40 characters.")] public string ClientSecret { get; set; } - //todo - will remove SKey and IKey with PM-8107 - [StringLength(50)] - public string IntegrationKey { get; set; } - //todo - will remove SKey and IKey with PM-8107 - [StringLength(50)] - public string SecretKey { get; set; } [Required] - [StringLength(50)] public string Host { get; set; } public User ToUser(User existingUser) @@ -65,22 +60,17 @@ public User ToUser(User existingUser) var providers = existingUser.GetTwoFactorProviders(); if (providers == null) { - providers = new Dictionary(); + providers = []; } else if (providers.ContainsKey(TwoFactorProviderType.Duo)) { providers.Remove(TwoFactorProviderType.Duo); } - Temporary_SyncDuoParams(); - providers.Add(TwoFactorProviderType.Duo, new TwoFactorProvider { MetaData = new Dictionary { - //todo - will remove SKey and IKey with PM-8107 - ["SKey"] = SecretKey, - ["IKey"] = IntegrationKey, ["ClientSecret"] = ClientSecret, ["ClientId"] = ClientId, ["Host"] = Host @@ -96,22 +86,17 @@ public Organization ToOrganization(Organization existingOrg) var providers = existingOrg.GetTwoFactorProviders(); if (providers == null) { - providers = new Dictionary(); + providers = []; } else if (providers.ContainsKey(TwoFactorProviderType.OrganizationDuo)) { providers.Remove(TwoFactorProviderType.OrganizationDuo); } - Temporary_SyncDuoParams(); - providers.Add(TwoFactorProviderType.OrganizationDuo, new TwoFactorProvider { MetaData = new Dictionary { - //todo - will remove SKey and IKey with PM-8107 - ["SKey"] = SecretKey, - ["IKey"] = IntegrationKey, ["ClientSecret"] = ClientSecret, ["ClientId"] = ClientId, ["Host"] = Host @@ -124,34 +109,22 @@ public Organization ToOrganization(Organization existingOrg) public override IEnumerable Validate(ValidationContext validationContext) { - if (!DuoApi.ValidHost(Host)) + var results = new List(); + if (string.IsNullOrWhiteSpace(ClientId)) { - yield return new ValidationResult("Host is invalid.", [nameof(Host)]); + results.Add(new ValidationResult("ClientId is required.", [nameof(ClientId)])); } - if (string.IsNullOrWhiteSpace(ClientSecret) && string.IsNullOrWhiteSpace(ClientId) && - string.IsNullOrWhiteSpace(SecretKey) && string.IsNullOrWhiteSpace(IntegrationKey)) - { - yield return new ValidationResult("Neither v2 or v4 values are valid.", [nameof(IntegrationKey), nameof(SecretKey), nameof(ClientSecret), nameof(ClientId)]); - } - } - /* - use this method to ensure that both v2 params and v4 params are in sync - todo will be removed in pm-8107 - */ - private void Temporary_SyncDuoParams() - { - // Even if IKey and SKey exist prioritize v4 params ClientId and ClientSecret - if (!string.IsNullOrWhiteSpace(ClientSecret) && !string.IsNullOrWhiteSpace(ClientId)) + if (string.IsNullOrWhiteSpace(ClientSecret)) { - SecretKey = ClientSecret; - IntegrationKey = ClientId; + results.Add(new ValidationResult("ClientSecret is required.", [nameof(ClientSecret)])); } - else if (!string.IsNullOrWhiteSpace(SecretKey) && !string.IsNullOrWhiteSpace(IntegrationKey)) + + if (string.IsNullOrWhiteSpace(Host) || !DuoUniversalTokenService.ValidDuoHost(Host)) { - ClientSecret = SecretKey; - ClientId = IntegrationKey; + results.Add(new ValidationResult("Host is invalid.", [nameof(Host)])); } + return results; } } diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs index 8b8c36d2e8b4..79012783a4df 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs @@ -13,37 +13,26 @@ public class TwoFactorDuoResponseModel : ResponseModel public TwoFactorDuoResponseModel(User user) : base(ResponseObj) { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } + ArgumentNullException.ThrowIfNull(user); var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); Build(provider); } - public TwoFactorDuoResponseModel(Organization org) + public TwoFactorDuoResponseModel(Organization organization) : base(ResponseObj) { - if (org == null) - { - throw new ArgumentNullException(nameof(org)); - } + ArgumentNullException.ThrowIfNull(organization); - var provider = org.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); + var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); Build(provider); } public bool Enabled { get; set; } public string Host { get; set; } - //TODO - will remove SecretKey with PM-8107 - public string SecretKey { get; set; } - //TODO - will remove IntegrationKey with PM-8107 - public string IntegrationKey { get; set; } public string ClientSecret { get; set; } public string ClientId { get; set; } - // updated build to assist in the EDD migration for the Duo 2FA provider private void Build(TwoFactorProvider provider) { if (provider?.MetaData != null && provider.MetaData.Count > 0) @@ -54,36 +43,13 @@ private void Build(TwoFactorProvider provider) { Host = (string)host; } - - //todo - will remove SKey and IKey with PM-8107 - // check Skey and IKey first if they exist - if (provider.MetaData.TryGetValue("SKey", out var sKey)) - { - ClientSecret = MaskKey((string)sKey); - SecretKey = MaskKey((string)sKey); - } - if (provider.MetaData.TryGetValue("IKey", out var iKey)) - { - IntegrationKey = (string)iKey; - ClientId = (string)iKey; - } - - // Even if IKey and SKey exist prioritize v4 params ClientId and ClientSecret if (provider.MetaData.TryGetValue("ClientSecret", out var clientSecret)) { - if (!string.IsNullOrWhiteSpace((string)clientSecret)) - { - ClientSecret = MaskKey((string)clientSecret); - SecretKey = MaskKey((string)clientSecret); - } + ClientSecret = MaskSecret((string)clientSecret); } if (provider.MetaData.TryGetValue("ClientId", out var clientId)) { - if (!string.IsNullOrWhiteSpace((string)clientId)) - { - ClientId = (string)clientId; - IntegrationKey = (string)clientId; - } + ClientId = (string)clientId; } } else @@ -92,30 +58,7 @@ private void Build(TwoFactorProvider provider) } } - /* - use this method to ensure that both v2 params and v4 params are in sync - todo will be removed in pm-8107 - */ - private void Temporary_SyncDuoParams() - { - // Even if IKey and SKey exist prioritize v4 params ClientId and ClientSecret - if (!string.IsNullOrWhiteSpace(ClientSecret) && !string.IsNullOrWhiteSpace(ClientId)) - { - SecretKey = ClientSecret; - IntegrationKey = ClientId; - } - else if (!string.IsNullOrWhiteSpace(SecretKey) && !string.IsNullOrWhiteSpace(IntegrationKey)) - { - ClientSecret = SecretKey; - ClientId = IntegrationKey; - } - else - { - throw new InvalidDataException("Invalid Duo parameters."); - } - } - - private static string MaskKey(string key) + private static string MaskSecret(string key) { if (string.IsNullOrWhiteSpace(key) || key.Length <= 6) { diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 7962215a44d5..65935440c512 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -23,7 +23,6 @@ using Bit.SharedWeb.Utilities; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.DependencyInjection.Extensions; -using Bit.Core.Auth.Identity; using Bit.Core.Auth.UserFeatures; using Bit.Core.Entities; using Bit.Core.Billing.Extensions; @@ -32,10 +31,10 @@ 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.ReportFeatures; - #if !OSS using Bit.Commercial.Core.SecretsManager; using Bit.Commercial.Core.Utilities; diff --git a/src/Core/Auth/Identity/DuoWebTokenProvider.cs b/src/Core/Auth/Identity/DuoWebTokenProvider.cs deleted file mode 100644 index 6ab020326284..000000000000 --- a/src/Core/Auth/Identity/DuoWebTokenProvider.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Models; -using Bit.Core.Auth.Utilities.Duo; -using Bit.Core.Entities; -using Bit.Core.Services; -using Bit.Core.Settings; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.DependencyInjection; - -namespace Bit.Core.Auth.Identity; - -public class DuoWebTokenProvider : IUserTwoFactorTokenProvider -{ - private readonly IServiceProvider _serviceProvider; - private readonly GlobalSettings _globalSettings; - - public DuoWebTokenProvider( - IServiceProvider serviceProvider, - GlobalSettings globalSettings) - { - _serviceProvider = serviceProvider; - _globalSettings = globalSettings; - } - - public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) - { - var userService = _serviceProvider.GetRequiredService(); - if (!(await userService.CanAccessPremium(user))) - { - return false; - } - - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); - if (!HasProperMetaData(provider)) - { - return false; - } - - return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Duo, user); - } - - public async Task GenerateAsync(string purpose, UserManager manager, User user) - { - var userService = _serviceProvider.GetRequiredService(); - if (!(await userService.CanAccessPremium(user))) - { - return null; - } - - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); - if (!HasProperMetaData(provider)) - { - return null; - } - - var signatureRequest = DuoWeb.SignRequest((string)provider.MetaData["IKey"], - (string)provider.MetaData["SKey"], _globalSettings.Duo.AKey, user.Email); - return signatureRequest; - } - - public async Task ValidateAsync(string purpose, string token, UserManager manager, User user) - { - var userService = _serviceProvider.GetRequiredService(); - if (!(await userService.CanAccessPremium(user))) - { - return false; - } - - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); - if (!HasProperMetaData(provider)) - { - return false; - } - - var response = DuoWeb.VerifyResponse((string)provider.MetaData["IKey"], (string)provider.MetaData["SKey"], - _globalSettings.Duo.AKey, token); - - return response == user.Email; - } - - private bool HasProperMetaData(TwoFactorProvider provider) - { - return provider?.MetaData != null && provider.MetaData.ContainsKey("IKey") && - provider.MetaData.ContainsKey("SKey") && provider.MetaData.ContainsKey("Host"); - } -} diff --git a/src/Core/Auth/Identity/OrganizationDuoWebTokenProvider.cs b/src/Core/Auth/Identity/OrganizationDuoWebTokenProvider.cs deleted file mode 100644 index 58bcf5efd8db..000000000000 --- a/src/Core/Auth/Identity/OrganizationDuoWebTokenProvider.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Models; -using Bit.Core.Auth.Utilities.Duo; -using Bit.Core.Entities; -using Bit.Core.Settings; - -namespace Bit.Core.Auth.Identity; - -public interface IOrganizationDuoWebTokenProvider : IOrganizationTwoFactorTokenProvider { } - -public class OrganizationDuoWebTokenProvider : IOrganizationDuoWebTokenProvider -{ - private readonly GlobalSettings _globalSettings; - - public OrganizationDuoWebTokenProvider(GlobalSettings globalSettings) - { - _globalSettings = globalSettings; - } - - public Task CanGenerateTwoFactorTokenAsync(Organization organization) - { - if (organization == null || !organization.Enabled || !organization.Use2fa) - { - return Task.FromResult(false); - } - - var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); - var canGenerate = organization.TwoFactorProviderIsEnabled(TwoFactorProviderType.OrganizationDuo) - && HasProperMetaData(provider); - return Task.FromResult(canGenerate); - } - - public Task GenerateAsync(Organization organization, User user) - { - if (organization == null || !organization.Enabled || !organization.Use2fa) - { - return Task.FromResult(null); - } - - var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); - if (!HasProperMetaData(provider)) - { - return Task.FromResult(null); - } - - var signatureRequest = DuoWeb.SignRequest(provider.MetaData["IKey"].ToString(), - provider.MetaData["SKey"].ToString(), _globalSettings.Duo.AKey, user.Email); - return Task.FromResult(signatureRequest); - } - - public Task ValidateAsync(string token, Organization organization, User user) - { - if (organization == null || !organization.Enabled || !organization.Use2fa) - { - return Task.FromResult(false); - } - - var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); - if (!HasProperMetaData(provider)) - { - return Task.FromResult(false); - } - - var response = DuoWeb.VerifyResponse(provider.MetaData["IKey"].ToString(), - provider.MetaData["SKey"].ToString(), _globalSettings.Duo.AKey, token); - - return Task.FromResult(response == user.Email); - } - - private bool HasProperMetaData(TwoFactorProvider provider) - { - return provider?.MetaData != null && provider.MetaData.ContainsKey("IKey") && - provider.MetaData.ContainsKey("SKey") && provider.MetaData.ContainsKey("Host"); - } -} diff --git a/src/Core/Auth/Identity/TemporaryDuoWebV4SDKService.cs b/src/Core/Auth/Identity/TemporaryDuoWebV4SDKService.cs deleted file mode 100644 index f78abdfd1363..000000000000 --- a/src/Core/Auth/Identity/TemporaryDuoWebV4SDKService.cs +++ /dev/null @@ -1,172 +0,0 @@ -using Bit.Core.Auth.Models; -using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Context; -using Bit.Core.Entities; -using Bit.Core.Settings; -using Bit.Core.Tokens; -using Microsoft.Extensions.Logging; -using Duo = DuoUniversal; - -namespace Bit.Core.Auth.Identity; - -/* - PM-5156 addresses tech debt - Interface to allow for DI, will end up being removed as part of the removal of the old Duo SDK v2 flows. - This service is to support SDK v4 flows for Duo. At some time in the future we will need - to combine this service with the DuoWebTokenProvider and OrganizationDuoWebTokenProvider to support SDK v4. -*/ -public interface ITemporaryDuoWebV4SDKService -{ - Task GenerateAsync(TwoFactorProvider provider, User user); - Task ValidateAsync(string token, TwoFactorProvider provider, User user); -} - -public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService -{ - private readonly ICurrentContext _currentContext; - private readonly GlobalSettings _globalSettings; - private readonly IDataProtectorTokenFactory _tokenDataFactory; - private readonly ILogger _logger; - - /// - /// Constructor for the DuoUniversalPromptService. Used to supplement v2 implementation of Duo with v4 SDK - /// - /// used to fetch initiating Client - /// used to fetch vault URL for Redirect URL - public TemporaryDuoWebV4SDKService( - ICurrentContext currentContext, - GlobalSettings globalSettings, - IDataProtectorTokenFactory tokenDataFactory, - ILogger logger) - { - _currentContext = currentContext; - _globalSettings = globalSettings; - _tokenDataFactory = tokenDataFactory; - _logger = logger; - } - - /// - /// Provider agnostic (either Duo or OrganizationDuo) method to generate a Duo Auth URL - /// - /// Either Duo or OrganizationDuo - /// self - /// AuthUrl for DUO SDK v4 - public async Task GenerateAsync(TwoFactorProvider provider, User user) - { - if (!HasProperMetaData(provider)) - { - if (!HasProperMetaData_SDKV2(provider)) - { - return null; - } - } - - - var duoClient = await BuildDuoClientAsync(provider); - if (duoClient == null) - { - return null; - } - - var state = _tokenDataFactory.Protect(new DuoUserStateTokenable(user)); - var authUrl = duoClient.GenerateAuthUri(user.Email, state); - - return authUrl; - } - - /// - /// Validates Duo SDK v4 response - /// - /// response form Duo - /// TwoFactorProviderType Duo or OrganizationDuo - /// self - /// true or false depending on result of verification - public async Task ValidateAsync(string token, TwoFactorProvider provider, User user) - { - if (!HasProperMetaData(provider)) - { - if (!HasProperMetaData_SDKV2(provider)) - { - return false; - } - } - - var duoClient = await BuildDuoClientAsync(provider); - if (duoClient == null) - { - return false; - } - - var parts = token.Split("|"); - var authCode = parts[0]; - var state = parts[1]; - - _tokenDataFactory.TryUnprotect(state, out var tokenable); - if (!tokenable.Valid || !tokenable.TokenIsValid(user)) - { - return false; - } - - // duoClient compares the email from the received IdToken with user.Email to verify a bad actor hasn't used - // their authCode with a victims credentials - var res = await duoClient.ExchangeAuthorizationCodeFor2faResult(authCode, user.Email); - // If the result of the exchange doesn't throw an exception and it's not null, then it's valid - return res.AuthResult.Result == "allow"; - } - - private bool HasProperMetaData(TwoFactorProvider provider) - { - return provider?.MetaData != null && provider.MetaData.ContainsKey("ClientId") && - provider.MetaData.ContainsKey("ClientSecret") && provider.MetaData.ContainsKey("Host"); - } - - /// - /// Checks if the metadata for SDK V2 is present. - /// Transitional method to support Duo during v4 database rename - /// - /// The TwoFactorProvider object to check. - /// True if the provider has the proper metadata; otherwise, false. - private bool HasProperMetaData_SDKV2(TwoFactorProvider provider) - { - if (provider?.MetaData != null && - provider.MetaData.TryGetValue("IKey", out var iKey) && - provider.MetaData.TryGetValue("SKey", out var sKey) && - provider.MetaData.ContainsKey("Host")) - { - provider.MetaData.Add("ClientId", iKey); - provider.MetaData.Add("ClientSecret", sKey); - return true; - } - else - { - return false; - } - } - - /// - /// Generates a Duo.Client object for use with Duo SDK v4. This combines the health check and the client generation - /// - /// TwoFactorProvider Duo or OrganizationDuo - /// Duo.Client object or null - private async Task BuildDuoClientAsync(TwoFactorProvider provider) - { - // Fetch Client name from header value since duo auth can be initiated from multiple clients and we want - // to redirect back to the initiating client - _currentContext.HttpContext.Request.Headers.TryGetValue("Bitwarden-Client-Name", out var bitwardenClientName); - var redirectUri = string.Format("{0}/duo-redirect-connector.html?client={1}", - _globalSettings.BaseServiceUri.Vault, bitwardenClientName.FirstOrDefault() ?? "web"); - - var client = new Duo.ClientBuilder( - (string)provider.MetaData["ClientId"], - (string)provider.MetaData["ClientSecret"], - (string)provider.MetaData["Host"], - redirectUri).Build(); - - if (!await client.DoHealthCheck(true)) - { - _logger.LogError("Unable to connect to Duo. Health check failed."); - return null; - } - return client; - } -} diff --git a/src/Core/Auth/Identity/AuthenticatorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs similarity index 97% rename from src/Core/Auth/Identity/AuthenticatorTokenProvider.cs rename to src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs index fae2d23b19dd..9468e4d57179 100644 --- a/src/Core/Auth/Identity/AuthenticatorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using OtpNet; -namespace Bit.Core.Auth.Identity; +namespace Bit.Core.Auth.Identity.TokenProviders; public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider { diff --git a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs new file mode 100644 index 000000000000..21311326c084 --- /dev/null +++ b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs @@ -0,0 +1,102 @@ +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Entities; +using Bit.Core.Services; +using Bit.Core.Tokens; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Duo = DuoUniversal; + +namespace Bit.Core.Auth.Identity.TokenProviders; + +public class DuoUniversalTokenProvider( + IServiceProvider serviceProvider, + IDataProtectorTokenFactory tokenDataFactory, + IDuoUniversalTokenService duoUniversalTokenService) : IUserTwoFactorTokenProvider +{ + /// + /// We need the IServiceProvider to resolve the IUserService. There is a complex dependency dance + /// occurring between IUserService, which extends the UserManager, and the usage of the + /// UserManager within this class. Trying to resolve the IUserService using the DI pipeline + /// will not allow the server to start and it will hang and give no helpful indication as to the problem. + /// + private readonly IServiceProvider _serviceProvider = serviceProvider; + private readonly IDataProtectorTokenFactory _tokenDataFactory = tokenDataFactory; + private readonly IDuoUniversalTokenService _duoUniversalTokenService = duoUniversalTokenService; + + public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) + { + var userService = _serviceProvider.GetRequiredService(); + var provider = await GetDuoTwoFactorProvider(user, userService); + if (provider == null) + { + return false; + } + return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Duo, user); + } + + public async Task GenerateAsync(string purpose, UserManager manager, User user) + { + var duoClient = await GetDuoClientAsync(user); + if (duoClient == null) + { + return null; + } + return _duoUniversalTokenService.GenerateAuthUrl(duoClient, _tokenDataFactory, user); + } + + public async Task ValidateAsync(string purpose, string token, UserManager manager, User user) + { + var duoClient = await GetDuoClientAsync(user); + if (duoClient == null) + { + return false; + } + return await _duoUniversalTokenService.RequestDuoValidationAsync(duoClient, _tokenDataFactory, user, token); + } + + /// + /// Get the Duo Two Factor Provider for the user if they have access to Duo + /// + /// Active User + /// null or Duo TwoFactorProvider + private async Task GetDuoTwoFactorProvider(User user, IUserService userService) + { + if (!await userService.CanAccessPremium(user)) + { + return null; + } + + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); + if (!_duoUniversalTokenService.HasProperDuoMetadata(provider)) + { + return null; + } + + return provider; + } + + /// + /// Uses the User to fetch a valid TwoFactorProvider and use it to create a Duo.Client + /// + /// active user + /// null or Duo TwoFactorProvider + private async Task GetDuoClientAsync(User user) + { + var userService = _serviceProvider.GetRequiredService(); + var provider = await GetDuoTwoFactorProvider(user, userService); + if (provider == null) + { + return null; + } + + var duoClient = await _duoUniversalTokenService.BuildDuoTwoFactorClientAsync(provider); + if (duoClient == null) + { + return null; + } + + return duoClient; + } +} diff --git a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenService.cs b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenService.cs new file mode 100644 index 000000000000..8dd07e7ee655 --- /dev/null +++ b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenService.cs @@ -0,0 +1,177 @@ +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Settings; +using Bit.Core.Tokens; +using Duo = DuoUniversal; + +namespace Bit.Core.Auth.Identity.TokenProviders; + +/// +/// OrganizationDuo and Duo TwoFactorProviderTypes both use the same flows so both of those Token Providers will +/// have this class injected to utilize these methods +/// +public interface IDuoUniversalTokenService +{ + /// + /// Generates the Duo Auth URL for the user to be redirected to Duo for 2FA. This + /// Auth URL also lets the Duo Service know where to redirect the user back to after + /// the 2FA process is complete. + /// + /// A not null valid Duo.Client + /// This service creates the state token for added security + /// currently active user + /// a URL in string format + string GenerateAuthUrl( + Duo.Client duoClient, + IDataProtectorTokenFactory tokenDataFactory, + User user); + + /// + /// Makes the request to Duo to validate the authCode and state token + /// + /// A not null valid Duo.Client + /// Factory for decrypting the state + /// self + /// token received from the client + /// boolean based on result from Duo + Task RequestDuoValidationAsync( + Duo.Client duoClient, + IDataProtectorTokenFactory tokenDataFactory, + User user, + string token); + + /// + /// Generates a Duo.Client object for use with Duo SDK v4. This method is to validate a Duo configuration + /// when adding or updating the configuration. This method makes a web request to Duo to verify the configuration. + /// Throws exception if configuration is invalid. + /// + /// Duo client Secret + /// Duo client Id + /// Duo host + /// Boolean + Task ValidateDuoConfiguration(string clientSecret, string clientId, string host); + + /// + /// Checks provider for the correct Duo metadata: ClientId, ClientSecret, and Host. Does no validation on the data. + /// it is assumed to be correct. The only way to have the data written to the Database is after verification + /// occurs. + /// + /// Host being checked for proper data + /// true if all three are present; false if one is missing or the host is incorrect + bool HasProperDuoMetadata(TwoFactorProvider provider); + + /// + /// Generates a Duo.Client object for use with Duo SDK v4. This combines the health check and the client generation. + /// This method is made public so that it is easier to test. If the method was private then there would not be an + /// easy way to mock the response. Since this makes a web request it is difficult to mock. + /// + /// TwoFactorProvider Duo or OrganizationDuo + /// Duo.Client object or null + Task BuildDuoTwoFactorClientAsync(TwoFactorProvider provider); +} + +public class DuoUniversalTokenService( + ICurrentContext currentContext, + GlobalSettings globalSettings) : IDuoUniversalTokenService +{ + private readonly ICurrentContext _currentContext = currentContext; + private readonly GlobalSettings _globalSettings = globalSettings; + + public string GenerateAuthUrl( + Duo.Client duoClient, + IDataProtectorTokenFactory tokenDataFactory, + User user) + { + var state = tokenDataFactory.Protect(new DuoUserStateTokenable(user)); + var authUrl = duoClient.GenerateAuthUri(user.Email, state); + + return authUrl; + } + + public async Task RequestDuoValidationAsync( + Duo.Client duoClient, + IDataProtectorTokenFactory tokenDataFactory, + User user, + string token) + { + var parts = token.Split("|"); + var authCode = parts[0]; + var state = parts[1]; + tokenDataFactory.TryUnprotect(state, out var tokenable); + if (!tokenable.Valid || !tokenable.TokenIsValid(user)) + { + return false; + } + + // duoClient compares the email from the received IdToken with user.Email to verify a bad actor hasn't used + // their authCode with a victims credentials + var res = await duoClient.ExchangeAuthorizationCodeFor2faResult(authCode, user.Email); + // If the result of the exchange doesn't throw an exception and it's not null, then it's valid + return res.AuthResult.Result == "allow"; + } + + public async Task ValidateDuoConfiguration(string clientSecret, string clientId, string host) + { + // Do some simple checks to ensure data integrity + if (!ValidDuoHost(host) || + string.IsNullOrWhiteSpace(clientSecret) || + string.IsNullOrWhiteSpace(clientId)) + { + return false; + } + // The AuthURI is not important for this health check so we pass in a non-empty string + var client = new Duo.ClientBuilder(clientId, clientSecret, host, "non-empty").Build(); + + // This could throw an exception, the false flag will allow the exception to bubble up + return await client.DoHealthCheck(false); + } + + public bool HasProperDuoMetadata(TwoFactorProvider provider) + { + return provider?.MetaData != null && + provider.MetaData.ContainsKey("ClientId") && + provider.MetaData.ContainsKey("ClientSecret") && + provider.MetaData.ContainsKey("Host") && + ValidDuoHost((string)provider.MetaData["Host"]); + } + + + /// + /// Checks the host string to make sure it meets Duo's Guidelines before attempting to create a Duo.Client. + /// + /// string representing the Duo Host + /// true if the host is valid false otherwise + public static bool ValidDuoHost(string host) + { + if (Uri.TryCreate($"https://{host}", UriKind.Absolute, out var uri)) + { + return (string.IsNullOrWhiteSpace(uri.PathAndQuery) || uri.PathAndQuery == "/") && + uri.Host.StartsWith("api-") && + (uri.Host.EndsWith(".duosecurity.com") || uri.Host.EndsWith(".duofederal.com")); + } + return false; + } + + public async Task BuildDuoTwoFactorClientAsync(TwoFactorProvider provider) + { + // Fetch Client name from header value since duo auth can be initiated from multiple clients and we want + // to redirect back to the initiating client + _currentContext.HttpContext.Request.Headers.TryGetValue("Bitwarden-Client-Name", out var bitwardenClientName); + var redirectUri = string.Format("{0}/duo-redirect-connector.html?client={1}", + _globalSettings.BaseServiceUri.Vault, bitwardenClientName.FirstOrDefault() ?? "web"); + + var client = new Duo.ClientBuilder( + (string)provider.MetaData["ClientId"], + (string)provider.MetaData["ClientSecret"], + (string)provider.MetaData["Host"], + redirectUri).Build(); + + if (!await client.DoHealthCheck(false)) + { + return null; + } + return client; + } +} diff --git a/src/Core/Auth/Identity/EmailTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs similarity index 97% rename from src/Core/Auth/Identity/EmailTokenProvider.cs rename to src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs index 1db9e13ee535..be94124c03eb 100644 --- a/src/Core/Auth/Identity/EmailTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; -namespace Bit.Core.Auth.Identity; +namespace Bit.Core.Auth.Identity.TokenProviders; public class EmailTokenProvider : IUserTwoFactorTokenProvider { diff --git a/src/Core/Auth/Identity/EmailTwoFactorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs similarity index 97% rename from src/Core/Auth/Identity/EmailTwoFactorTokenProvider.cs rename to src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs index 607d86a13a8f..b0ad9bd4801b 100644 --- a/src/Core/Auth/Identity/EmailTwoFactorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; -namespace Bit.Core.Auth.Identity; +namespace Bit.Core.Auth.Identity.TokenProviders; public class EmailTwoFactorTokenProvider : EmailTokenProvider { diff --git a/src/Core/Auth/Identity/IOrganizationTwoFactorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/IOrganizationTwoFactorTokenProvider.cs similarity index 87% rename from src/Core/Auth/Identity/IOrganizationTwoFactorTokenProvider.cs rename to src/Core/Auth/Identity/TokenProviders/IOrganizationTwoFactorTokenProvider.cs index 4226cd036165..7e2a8c2ac2d2 100644 --- a/src/Core/Auth/Identity/IOrganizationTwoFactorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/IOrganizationTwoFactorTokenProvider.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Entities; -namespace Bit.Core.Auth.Identity; +namespace Bit.Core.Auth.Identity.TokenProviders; public interface IOrganizationTwoFactorTokenProvider { diff --git a/src/Core/Auth/Identity/TokenProviders/OrganizationDuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/OrganizationDuoUniversalTokenProvider.cs new file mode 100644 index 000000000000..c8007dd6ec14 --- /dev/null +++ b/src/Core/Auth/Identity/TokenProviders/OrganizationDuoUniversalTokenProvider.cs @@ -0,0 +1,81 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Entities; +using Bit.Core.Tokens; +using Duo = DuoUniversal; + +namespace Bit.Core.Auth.Identity.TokenProviders; + +public interface IOrganizationDuoUniversalTokenProvider : IOrganizationTwoFactorTokenProvider { } + +public class OrganizationDuoUniversalTokenProvider( + IDataProtectorTokenFactory tokenDataFactory, + IDuoUniversalTokenService duoUniversalTokenService) : IOrganizationDuoUniversalTokenProvider +{ + private readonly IDataProtectorTokenFactory _tokenDataFactory = tokenDataFactory; + private readonly IDuoUniversalTokenService _duoUniversalTokenService = duoUniversalTokenService; + + public Task CanGenerateTwoFactorTokenAsync(Organization organization) + { + var provider = GetDuoTwoFactorProvider(organization); + if (provider != null && provider.Enabled) + { + return Task.FromResult(true); + } + return Task.FromResult(false); + } + + public async Task GenerateAsync(Organization organization, User user) + { + var duoClient = await GetDuoClientAsync(organization); + if (duoClient == null) + { + return null; + } + return _duoUniversalTokenService.GenerateAuthUrl(duoClient, _tokenDataFactory, user); + } + + public async Task ValidateAsync(string token, Organization organization, User user) + { + var duoClient = await GetDuoClientAsync(organization); + if (duoClient == null) + { + return false; + } + return await _duoUniversalTokenService.RequestDuoValidationAsync(duoClient, _tokenDataFactory, user, token); + } + + private TwoFactorProvider GetDuoTwoFactorProvider(Organization organization) + { + if (organization == null || !organization.Enabled || !organization.Use2fa) + { + return null; + } + + var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); + if (!_duoUniversalTokenService.HasProperDuoMetadata(provider)) + { + return null; + } + return provider; + } + + private async Task GetDuoClientAsync(Organization organization) + { + var provider = GetDuoTwoFactorProvider(organization); + if (provider == null) + { + return null; + } + + var duoClient = await _duoUniversalTokenService.BuildDuoTwoFactorClientAsync(provider); + if (duoClient == null) + { + return null; + } + + return duoClient; + } +} diff --git a/src/Core/Auth/Identity/TwoFactorRememberTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/TwoFactorRememberTokenProvider.cs similarity index 92% rename from src/Core/Auth/Identity/TwoFactorRememberTokenProvider.cs rename to src/Core/Auth/Identity/TokenProviders/TwoFactorRememberTokenProvider.cs index cf35eb2eb8ed..44b5d48b8249 100644 --- a/src/Core/Auth/Identity/TwoFactorRememberTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/TwoFactorRememberTokenProvider.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Bit.Core.Auth.Identity; +namespace Bit.Core.Auth.Identity.TokenProviders; public class TwoFactorRememberTokenProvider : DataProtectorTokenProvider { diff --git a/src/Core/Auth/Identity/WebAuthnTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs similarity index 99% rename from src/Core/Auth/Identity/WebAuthnTokenProvider.cs rename to src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs index a3b4aebea529..202ba3a38cc7 100644 --- a/src/Core/Auth/Identity/WebAuthnTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -namespace Bit.Core.Auth.Identity; +namespace Bit.Core.Auth.Identity.TokenProviders; public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider { diff --git a/src/Core/Auth/Identity/YubicoOtpTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs similarity index 93% rename from src/Core/Auth/Identity/YubicoOtpTokenProvider.cs rename to src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs index 75785118b8dc..9794a51ae920 100644 --- a/src/Core/Auth/Identity/YubicoOtpTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using YubicoDotNetClient; -namespace Bit.Core.Auth.Identity; +namespace Bit.Core.Auth.Identity.TokenProviders; public class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider { @@ -24,7 +24,7 @@ public YubicoOtpTokenProvider( public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { var userService = _serviceProvider.GetRequiredService(); - if (!(await userService.CanAccessPremium(user))) + if (!await userService.CanAccessPremium(user)) { return false; } @@ -46,7 +46,7 @@ public Task GenerateAsync(string purpose, UserManager manager, Use public async Task ValidateAsync(string purpose, string token, UserManager manager, User user) { var userService = _serviceProvider.GetRequiredService(); - if (!(await userService.CanAccessPremium(user))) + if (!await userService.CanAccessPremium(user)) { return false; } diff --git a/src/Core/Auth/Utilities/DuoApi.cs b/src/Core/Auth/Utilities/DuoApi.cs deleted file mode 100644 index 8bf5f16a91b4..000000000000 --- a/src/Core/Auth/Utilities/DuoApi.cs +++ /dev/null @@ -1,277 +0,0 @@ -/* -Original source modified from https://github.com/duosecurity/duo_api_csharp - -============================================================================= -============================================================================= - -Copyright (c) 2018 Duo Security -All rights reserved -*/ - -using System.Globalization; -using System.Net; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using System.Text.RegularExpressions; -using System.Web; -using Bit.Core.Models.Api.Response.Duo; - -namespace Bit.Core.Auth.Utilities; - -public class DuoApi -{ - private const string UrlScheme = "https"; - private const string UserAgent = "Bitwarden_DuoAPICSharp/1.0 (.NET Core)"; - - private readonly string _host; - private readonly string _ikey; - private readonly string _skey; - - private readonly HttpClient _httpClient = new(); - - public DuoApi(string ikey, string skey, string host) - { - _ikey = ikey; - _skey = skey; - _host = host; - - if (!ValidHost(host)) - { - throw new DuoException("Invalid Duo host configured.", new ArgumentException(nameof(host))); - } - } - - public static bool ValidHost(string host) - { - if (Uri.TryCreate($"https://{host}", UriKind.Absolute, out var uri)) - { - return (string.IsNullOrWhiteSpace(uri.PathAndQuery) || uri.PathAndQuery == "/") && - uri.Host.StartsWith("api-") && - (uri.Host.EndsWith(".duosecurity.com") || uri.Host.EndsWith(".duofederal.com")); - } - return false; - } - - public static string CanonicalizeParams(Dictionary parameters) - { - var ret = new List(); - foreach (var pair in parameters) - { - var p = string.Format("{0}={1}", HttpUtility.UrlEncode(pair.Key), HttpUtility.UrlEncode(pair.Value)); - // Signatures require upper-case hex digits. - p = Regex.Replace(p, "(%[0-9A-Fa-f][0-9A-Fa-f])", c => c.Value.ToUpperInvariant()); - // Escape only the expected characters. - p = Regex.Replace(p, "([!'()*])", c => "%" + Convert.ToByte(c.Value[0]).ToString("X")); - p = p.Replace("%7E", "~"); - // UrlEncode converts space (" ") to "+". The - // signature algorithm requires "%20" instead. Actual - // + has already been replaced with %2B. - p = p.Replace("+", "%20"); - ret.Add(p); - } - - ret.Sort(StringComparer.Ordinal); - return string.Join("&", ret.ToArray()); - } - - protected string CanonicalizeRequest(string method, string path, string canonParams, string date) - { - string[] lines = { - date, - method.ToUpperInvariant(), - _host.ToLower(), - path, - canonParams, - }; - return string.Join("\n", lines); - } - - public string Sign(string method, string path, string canonParams, string date) - { - var canon = CanonicalizeRequest(method, path, canonParams, date); - var sig = HmacSign(canon); - var auth = string.Concat(_ikey, ':', sig); - return string.Concat("Basic ", Encode64(auth)); - } - - /// The request timeout, in milliseconds. - /// Specify 0 to use the system-default timeout. Use caution if - /// you choose to specify a custom timeout - some API - /// calls (particularly in the Auth APIs) will not - /// return a response until an out-of-band authentication process - /// has completed. In some cases, this may take as much as a - /// small number of minutes. - private async Task<(string result, HttpStatusCode statusCode)> ApiCall(string method, string path, Dictionary parameters, int timeout) - { - if (parameters == null) - { - parameters = new Dictionary(); - } - - var canonParams = CanonicalizeParams(parameters); - var query = string.Empty; - if (!method.Equals("POST") && !method.Equals("PUT")) - { - if (parameters.Count > 0) - { - query = "?" + canonParams; - } - } - var url = $"{UrlScheme}://{_host}{path}{query}"; - - var dateString = RFC822UtcNow(); - var auth = Sign(method, path, canonParams, dateString); - - var request = new HttpRequestMessage - { - Method = new HttpMethod(method), - RequestUri = new Uri(url), - }; - request.Headers.Add("Authorization", auth); - request.Headers.Add("X-Duo-Date", dateString); - request.Headers.UserAgent.ParseAdd(UserAgent); - - if (timeout > 0) - { - _httpClient.Timeout = TimeSpan.FromMilliseconds(timeout); - } - - if (method.Equals("POST") || method.Equals("PUT")) - { - request.Content = new StringContent(canonParams, Encoding.UTF8, "application/x-www-form-urlencoded"); - } - - var response = await _httpClient.SendAsync(request); - var result = await response.Content.ReadAsStringAsync(); - var statusCode = response.StatusCode; - return (result, statusCode); - } - - public async Task JSONApiCall(string method, string path, Dictionary parameters = null) - { - return await JSONApiCall(method, path, parameters, 0); - } - - /// The request timeout, in milliseconds. - /// Specify 0 to use the system-default timeout. Use caution if - /// you choose to specify a custom timeout - some API - /// calls (particularly in the Auth APIs) will not - /// return a response until an out-of-band authentication process - /// has completed. In some cases, this may take as much as a - /// small number of minutes. - private async Task JSONApiCall(string method, string path, Dictionary parameters, int timeout) - { - var (res, statusCode) = await ApiCall(method, path, parameters, timeout); - try - { - var obj = JsonSerializer.Deserialize(res); - if (obj.Stat == "OK") - { - return obj.Response; - } - - throw new ApiException(obj.Code ?? 0, (int)statusCode, obj.Message, obj.MessageDetail); - } - catch (ApiException) - { - throw; - } - catch (Exception e) - { - throw new BadResponseException((int)statusCode, e); - } - } - - private int? ToNullableInt(string s) - { - int i; - if (int.TryParse(s, out i)) - { - return i; - } - return null; - } - - private string HmacSign(string data) - { - var keyBytes = Encoding.ASCII.GetBytes(_skey); - var dataBytes = Encoding.ASCII.GetBytes(data); - - using (var hmac = new HMACSHA1(keyBytes)) - { - var hash = hmac.ComputeHash(dataBytes); - var hex = BitConverter.ToString(hash); - return hex.Replace("-", string.Empty).ToLower(); - } - } - - private static string Encode64(string plaintext) - { - var plaintextBytes = Encoding.ASCII.GetBytes(plaintext); - return Convert.ToBase64String(plaintextBytes); - } - - private static string RFC822UtcNow() - { - // Can't use the "zzzz" format because it adds a ":" - // between the offset's hours and minutes. - var dateString = DateTime.UtcNow.ToString("ddd, dd MMM yyyy HH:mm:ss", CultureInfo.InvariantCulture); - var offset = 0; - var zone = "+" + offset.ToString(CultureInfo.InvariantCulture).PadLeft(2, '0'); - dateString += " " + zone.PadRight(5, '0'); - return dateString; - } -} - -public class DuoException : Exception -{ - public int HttpStatus { get; private set; } - - public DuoException(string message, Exception inner) - : base(message, inner) - { } - - public DuoException(int httpStatus, string message, Exception inner) - : base(message, inner) - { - HttpStatus = httpStatus; - } -} - -public class ApiException : DuoException -{ - public int Code { get; private set; } - public string ApiMessage { get; private set; } - public string ApiMessageDetail { get; private set; } - - public ApiException(int code, int httpStatus, string apiMessage, string apiMessageDetail) - : base(httpStatus, FormatMessage(code, apiMessage, apiMessageDetail), null) - { - Code = code; - ApiMessage = apiMessage; - ApiMessageDetail = apiMessageDetail; - } - - private static string FormatMessage(int code, string apiMessage, string apiMessageDetail) - { - return string.Format("Duo API Error {0}: '{1}' ('{2}')", code, apiMessage, apiMessageDetail); - } -} - -public class BadResponseException : DuoException -{ - public BadResponseException(int httpStatus, Exception inner) - : base(httpStatus, FormatMessage(httpStatus, inner), inner) - { } - - private static string FormatMessage(int httpStatus, Exception inner) - { - var innerMessage = "(null)"; - if (inner != null) - { - innerMessage = string.Format("'{0}'", inner.Message); - } - return string.Format("Got error {0} with HTTP Status {1}", innerMessage, httpStatus); - } -} diff --git a/src/Core/Auth/Utilities/DuoWeb.cs b/src/Core/Auth/Utilities/DuoWeb.cs deleted file mode 100644 index 98fa974ab28a..000000000000 --- a/src/Core/Auth/Utilities/DuoWeb.cs +++ /dev/null @@ -1,240 +0,0 @@ -/* -Original source modified from https://github.com/duosecurity/duo_dotnet - -============================================================================= -============================================================================= - -ref: https://github.com/duosecurity/duo_dotnet/blob/master/LICENSE - -Copyright (c) 2011, Duo Security, Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -3. The name of the author may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -using System.Security.Cryptography; -using System.Text; - -namespace Bit.Core.Auth.Utilities.Duo; - -public static class DuoWeb -{ - private const string DuoProfix = "TX"; - private const string AppPrefix = "APP"; - private const string AuthPrefix = "AUTH"; - private const int DuoExpire = 300; - private const int AppExpire = 3600; - private const int IKeyLength = 20; - private const int SKeyLength = 40; - private const int AKeyLength = 40; - - public static string ErrorUser = "ERR|The username passed to sign_request() is invalid."; - public static string ErrorIKey = "ERR|The Duo integration key passed to sign_request() is invalid."; - public static string ErrorSKey = "ERR|The Duo secret key passed to sign_request() is invalid."; - public static string ErrorAKey = "ERR|The application secret key passed to sign_request() must be at least " + - "40 characters."; - public static string ErrorUnknown = "ERR|An unknown error has occurred."; - - // throw on invalid bytes - private static Encoding _encoding = new UTF8Encoding(false, true); - private static DateTime _epoc = new DateTime(1970, 1, 1); - - /// - /// Generate a signed request for Duo authentication. - /// The returned value should be passed into the Duo.init() call - /// in the rendered web page used for Duo authentication. - /// - /// Duo integration key - /// Duo secret key - /// Application secret key - /// Primary-authenticated username - /// (optional) The current UTC time - /// signed request - public static string SignRequest(string ikey, string skey, string akey, string username, - DateTime? currentTime = null) - { - string duoSig; - string appSig; - - var currentTimeValue = currentTime ?? DateTime.UtcNow; - - if (username == string.Empty) - { - return ErrorUser; - } - if (username.Contains("|")) - { - return ErrorUser; - } - if (ikey.Length != IKeyLength) - { - return ErrorIKey; - } - if (skey.Length != SKeyLength) - { - return ErrorSKey; - } - if (akey.Length < AKeyLength) - { - return ErrorAKey; - } - - try - { - duoSig = SignVals(skey, username, ikey, DuoProfix, DuoExpire, currentTimeValue); - appSig = SignVals(akey, username, ikey, AppPrefix, AppExpire, currentTimeValue); - } - catch - { - return ErrorUnknown; - } - - return $"{duoSig}:{appSig}"; - } - - /// - /// Validate the signed response returned from Duo. - /// Returns the username of the authenticated user, or null. - /// - /// Duo integration key - /// Duo secret key - /// Application secret key - /// The signed response POST'ed to the server - /// (optional) The current UTC time - /// authenticated username, or null - public static string VerifyResponse(string ikey, string skey, string akey, string sigResponse, - DateTime? currentTime = null) - { - string authUser = null; - string appUser = null; - var currentTimeValue = currentTime ?? DateTime.UtcNow; - - try - { - var sigs = sigResponse.Split(':'); - var authSig = sigs[0]; - var appSig = sigs[1]; - - authUser = ParseVals(skey, authSig, AuthPrefix, ikey, currentTimeValue); - appUser = ParseVals(akey, appSig, AppPrefix, ikey, currentTimeValue); - } - catch - { - return null; - } - - if (authUser != appUser) - { - return null; - } - - return authUser; - } - - private static string SignVals(string key, string username, string ikey, string prefix, long expire, - DateTime currentTime) - { - var ts = (long)(currentTime - _epoc).TotalSeconds; - expire = ts + expire; - var val = $"{username}|{ikey}|{expire.ToString()}"; - var cookie = $"{prefix}|{Encode64(val)}"; - var sig = Sign(key, cookie); - return $"{cookie}|{sig}"; - } - - private static string ParseVals(string key, string val, string prefix, string ikey, DateTime currentTime) - { - var ts = (long)(currentTime - _epoc).TotalSeconds; - - var parts = val.Split('|'); - if (parts.Length != 3) - { - return null; - } - - var uPrefix = parts[0]; - var uB64 = parts[1]; - var uSig = parts[2]; - - var sig = Sign(key, $"{uPrefix}|{uB64}"); - if (Sign(key, sig) != Sign(key, uSig)) - { - return null; - } - - if (uPrefix != prefix) - { - return null; - } - - var cookie = Decode64(uB64); - var cookieParts = cookie.Split('|'); - if (cookieParts.Length != 3) - { - return null; - } - - var username = cookieParts[0]; - var uIKey = cookieParts[1]; - var expire = cookieParts[2]; - - if (uIKey != ikey) - { - return null; - } - - var expireTs = Convert.ToInt32(expire); - if (ts >= expireTs) - { - return null; - } - - return username; - } - - private static string Sign(string skey, string data) - { - var keyBytes = Encoding.ASCII.GetBytes(skey); - var dataBytes = Encoding.ASCII.GetBytes(data); - - using (var hmac = new HMACSHA1(keyBytes)) - { - var hash = hmac.ComputeHash(dataBytes); - var hex = BitConverter.ToString(hash); - return hex.Replace("-", "").ToLower(); - } - } - - private static string Encode64(string plaintext) - { - var plaintextBytes = _encoding.GetBytes(plaintext); - return Convert.ToBase64String(plaintextBytes); - } - - private static string Decode64(string encoded) - { - var plaintextBytes = Convert.FromBase64String(encoded); - return _encoding.GetString(plaintextBytes); - } -} diff --git a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs index 4cabcf4fa71e..e2c6406c899c 100644 --- a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs @@ -1,9 +1,7 @@ - -using System.Text.Json; -using Bit.Core; +using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Identity; +using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Context; @@ -52,8 +50,7 @@ public interface ITwoFactorAuthenticationValidator public class TwoFactorAuthenticationValidator( IUserService userService, UserManager userManager, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, + IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider, IFeatureService featureService, IApplicationCacheService applicationCacheService, IOrganizationUserRepository organizationUserRepository, @@ -63,8 +60,7 @@ public class TwoFactorAuthenticationValidator( { private readonly IUserService _userService = userService; private readonly UserManager _userManager = userManager; - private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider = organizationDuoWebTokenProvider; - private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService = duoWebV4SDKService; + private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider = organizationDuoWebTokenProvider; private readonly IFeatureService _featureService = featureService; private readonly IApplicationCacheService _applicationCacheService = applicationCacheService; private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository; @@ -153,17 +149,7 @@ public async Task VerifyTwoFactor( { if (organization.TwoFactorProviderIsEnabled(type)) { - // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt - if (_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) - { - if (!token.Contains(':')) - { - // We have to send the provider to the DuoWebV4SDKService to create the DuoClient - var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); - return await _duoWebV4SDKService.ValidateAsync(token, provider, user); - } - } - return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user); + return await _organizationDuoUniversalTokenProvider.ValidateAsync(token, organization, user); } return false; } @@ -181,19 +167,6 @@ public async Task VerifyTwoFactor( { return false; } - // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt - if (_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) - { - if (type == TwoFactorProviderType.Duo) - { - if (!token.Contains(':')) - { - // We have to send the provider to the DuoWebV4SDKService to create the DuoClient - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); - return await _duoWebV4SDKService.ValidateAsync(token, provider, user); - } - } - } return await _userManager.VerifyTwoFactorTokenAsync(user, CoreHelpers.CustomProviderName(type), token); default: @@ -248,10 +221,11 @@ private async Task> BuildTwoFactorParams(Organization in the future the `AuthUrl` will be the generated "token" - PM-8107 */ if (type == TwoFactorProviderType.OrganizationDuo && - await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization)) + await _organizationDuoUniversalTokenProvider.CanGenerateTwoFactorTokenAsync(organization)) { twoFactorParams.Add("Host", provider.MetaData["Host"]); - twoFactorParams.Add("AuthUrl", await _duoWebV4SDKService.GenerateAsync(provider, user)); + twoFactorParams.Add("AuthUrl", + await _organizationDuoUniversalTokenProvider.GenerateAsync(organization, user)); return twoFactorParams; } @@ -261,13 +235,9 @@ await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organizati CoreHelpers.CustomProviderName(type)); switch (type) { - /* - Note: Duo is in the midst of being updated to use the UserManager built-in TwoFactor class - in the future the `AuthUrl` will be the generated "token" - PM-8107 - */ case TwoFactorProviderType.Duo: twoFactorParams.Add("Host", provider.MetaData["Host"]); - twoFactorParams.Add("AuthUrl", await _duoWebV4SDKService.GenerateAsync(provider, user)); + twoFactorParams.Add("AuthUrl", token); break; case TwoFactorProviderType.WebAuthn: if (token != null) diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 1b99b4cc87d0..ab4108bfef34 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -10,6 +10,7 @@ using Bit.Core.AdminConsole.Services.NoopImplementations; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity; +using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.IdentityServer; using Bit.Core.Auth.LoginFeatures; using Bit.Core.Auth.Models.Business.Tokenables; @@ -113,6 +114,7 @@ public static void AddBaseServices(this IServiceCollection services, IGlobalSett services.AddSingleton(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddLoginServices(); services.AddScoped(); @@ -388,8 +390,7 @@ public static void AddNoopServices(this IServiceCollection services) public static IdentityBuilder AddCustomIdentityServices( this IServiceCollection services, GlobalSettings globalSettings) { - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.Configure(options => options.IterationCount = 100000); services.Configure(options => { @@ -430,7 +431,7 @@ public static IdentityBuilder AddCustomIdentityServices( CoreHelpers.CustomProviderName(TwoFactorProviderType.Email)) .AddTokenProvider( CoreHelpers.CustomProviderName(TwoFactorProviderType.YubiKey)) - .AddTokenProvider( + .AddTokenProvider( CoreHelpers.CustomProviderName(TwoFactorProviderType.Duo)) .AddTokenProvider( CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember)) diff --git a/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs b/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs new file mode 100644 index 000000000000..11c8288ed920 --- /dev/null +++ b/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs @@ -0,0 +1,295 @@ +using Bit.Api.Auth.Controllers; +using Bit.Api.Auth.Models.Request; +using Bit.Api.Auth.Models.Request.Accounts; +using Bit.Api.Auth.Models.Response.TwoFactor; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Identity.TokenProviders; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +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.Api.Test.Auth.Controllers; + +[ControllerCustomize(typeof(TwoFactorController))] +[SutProviderCustomize] +public class TwoFactorControllerTests +{ + [Theory, BitAutoData] + public async Task CheckAsync_UserNull_ThrowsUnauthorizedException(SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetUserByPrincipalAsync(default) + .ReturnsForAnyArgs(null as User); + + // Act + var result = () => sutProvider.Sut.GetDuo(request); + + // Assert + await Assert.ThrowsAsync(result); + } + + [Theory, BitAutoData] + public async Task CheckAsync_BadSecret_ThrowsBadRequestException(User user, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetUserByPrincipalAsync(default) + .ReturnsForAnyArgs(user); + + sutProvider.GetDependency() + .VerifySecretAsync(default, default) + .ReturnsForAnyArgs(false); + + // Act + try + { + await sutProvider.Sut.GetDuo(request); + } + catch (BadRequestException e) + { + // Assert + Assert.Equal("The model state is invalid.", e.Message); + } + } + + [Theory, BitAutoData] + public async Task CheckAsync_CannotAccessPremium_ThrowsBadRequestException(User user, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetUserByPrincipalAsync(default) + .ReturnsForAnyArgs(user); + + sutProvider.GetDependency() + .VerifySecretAsync(default, default) + .ReturnsForAnyArgs(true); + + sutProvider.GetDependency() + .CanAccessPremium(default) + .ReturnsForAnyArgs(false); + + // Act + try + { + await sutProvider.Sut.GetDuo(request); + } + catch (BadRequestException e) + { + // Assert + Assert.Equal("Premium status is required.", e.Message); + } + } + + [Theory, BitAutoData] + public async Task GetDuo_Success(User user, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + user.TwoFactorProviders = GetUserTwoFactorDuoProvidersJson(); + SetupCheckAsyncToPass(sutProvider, user); + + // Act + var result = await sutProvider.Sut.GetDuo(request); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task PutDuo_InvalidConfiguration_ThrowsBadRequestException(User user, UpdateTwoFactorDuoRequestModel request, SutProvider sutProvider) + { + // Arrange + SetupCheckAsyncToPass(sutProvider, user); + sutProvider.GetDependency() + .ValidateDuoConfiguration(default, default, default) + .Returns(false); + + // Act + try + { + await sutProvider.Sut.PutDuo(request); + } + catch (BadRequestException e) + { + // Assert + Assert.Equal("Duo configuration settings are not valid. Please re-check the Duo Admin panel.", e.Message); + } + } + + [Theory, BitAutoData] + public async Task PutDuo_Success(User user, UpdateTwoFactorDuoRequestModel request, SutProvider sutProvider) + { + // Arrange + user.TwoFactorProviders = GetUserTwoFactorDuoProvidersJson(); + SetupCheckAsyncToPass(sutProvider, user); + + sutProvider.GetDependency() + .ValidateDuoConfiguration(default, default, default) + .ReturnsForAnyArgs(true); + + // Act + var result = await sutProvider.Sut.PutDuo(request); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal(user.TwoFactorProviders, request.ToUser(user).TwoFactorProviders); + } + + [Theory, BitAutoData] + public async Task CheckOrganizationAsync_ManagePolicies_ThrowsNotFoundException( + User user, Organization organization, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + organization.TwoFactorProviders = GetOrganizationTwoFactorDuoProvidersJson(); + SetupCheckAsyncToPass(sutProvider, user); + + sutProvider.GetDependency() + .ManagePolicies(default) + .ReturnsForAnyArgs(false); + + // Act + var result = () => sutProvider.Sut.GetOrganizationDuo(organization.Id.ToString(), request); + + // Assert + await Assert.ThrowsAsync(result); + } + + [Theory, BitAutoData] + public async Task CheckOrganizationAsync_GetByIdAsync_ThrowsNotFoundException( + User user, Organization organization, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + organization.TwoFactorProviders = GetOrganizationTwoFactorDuoProvidersJson(); + SetupCheckAsyncToPass(sutProvider, user); + + sutProvider.GetDependency() + .ManagePolicies(default) + .ReturnsForAnyArgs(true); + + sutProvider.GetDependency() + .GetByIdAsync(default) + .ReturnsForAnyArgs(null as Organization); + + // Act + var result = () => sutProvider.Sut.GetOrganizationDuo(organization.Id.ToString(), request); + + // Assert + await Assert.ThrowsAsync(result); + } + + [Theory, BitAutoData] + public async Task GetOrganizationDuo_Success( + User user, Organization organization, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + organization.TwoFactorProviders = GetOrganizationTwoFactorDuoProvidersJson(); + SetupCheckAsyncToPass(sutProvider, user); + SetupCheckOrganizationAsyncToPass(sutProvider, organization); + + // Act + var result = await sutProvider.Sut.GetOrganizationDuo(organization.Id.ToString(), request); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task PutOrganizationDuo_InvalidConfiguration_ThrowsBadRequestException( + User user, Organization organization, UpdateTwoFactorDuoRequestModel request, SutProvider sutProvider) + { + // Arrange + SetupCheckAsyncToPass(sutProvider, user); + SetupCheckOrganizationAsyncToPass(sutProvider, organization); + + sutProvider.GetDependency() + .ValidateDuoConfiguration(default, default, default) + .ReturnsForAnyArgs(false); + + // Act + try + { + await sutProvider.Sut.PutOrganizationDuo(organization.Id.ToString(), request); + } + catch (BadRequestException e) + { + // Assert + Assert.Equal("Duo configuration settings are not valid. Please re-check the Duo Admin panel.", e.Message); + } + } + + [Theory, BitAutoData] + public async Task PutOrganizationDuo_Success( + User user, Organization organization, UpdateTwoFactorDuoRequestModel request, SutProvider sutProvider) + { + // Arrange + SetupCheckAsyncToPass(sutProvider, user); + SetupCheckOrganizationAsyncToPass(sutProvider, organization); + organization.TwoFactorProviders = GetUserTwoFactorDuoProvidersJson(); + + sutProvider.GetDependency() + .ValidateDuoConfiguration(default, default, default) + .ReturnsForAnyArgs(true); + + // Act + var result = + await sutProvider.Sut.PutOrganizationDuo(organization.Id.ToString(), request); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal(organization.TwoFactorProviders, request.ToOrganization(organization).TwoFactorProviders); + } + + + private string GetUserTwoFactorDuoProvidersJson() + { + return + "{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; + } + + private string GetOrganizationTwoFactorDuoProvidersJson() + { + return + "{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; + } + + /// + /// Sets up the CheckAsync method to pass. + /// + /// uses bit auto data + /// uses bit auto data + private void SetupCheckAsyncToPass(SutProvider sutProvider, User user) + { + sutProvider.GetDependency() + .GetUserByPrincipalAsync(default) + .ReturnsForAnyArgs(user); + + sutProvider.GetDependency() + .VerifySecretAsync(default, default) + .ReturnsForAnyArgs(true); + + sutProvider.GetDependency() + .CanAccessPremium(default) + .ReturnsForAnyArgs(true); + } + + private void SetupCheckOrganizationAsyncToPass(SutProvider sutProvider, Organization organization) + { + sutProvider.GetDependency() + .ManagePolicies(default) + .ReturnsForAnyArgs(true); + + sutProvider.GetDependency() + .GetByIdAsync(default) + .ReturnsForAnyArgs(organization); + } +} diff --git a/test/Api.Test/Auth/Models/Request/OrganizationTwoFactorDuoRequestModelTests.cs b/test/Api.Test/Auth/Models/Request/OrganizationTwoFactorDuoRequestModelTests.cs index 5fbaf88671c5..361adea536d8 100644 --- a/test/Api.Test/Auth/Models/Request/OrganizationTwoFactorDuoRequestModelTests.cs +++ b/test/Api.Test/Auth/Models/Request/OrganizationTwoFactorDuoRequestModelTests.cs @@ -18,8 +18,6 @@ public void ShouldAddOrUpdateTwoFactorProvider_WhenExistingProviderDoesNotExist( { ClientId = "clientId", ClientSecret = "clientSecret", - IntegrationKey = "integrationKey", - SecretKey = "secretKey", Host = "example.com" }; @@ -30,8 +28,6 @@ public void ShouldAddOrUpdateTwoFactorProvider_WhenExistingProviderDoesNotExist( Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo)); Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientId"]); Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientSecret"]); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["IKey"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["SKey"]); Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["Host"]); Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled); } @@ -49,8 +45,6 @@ public void ShouldUpdateTwoFactorProvider_WhenExistingProviderExists() { ClientId = "newClientId", ClientSecret = "newClientSecret", - IntegrationKey = "newIntegrationKey", - SecretKey = "newSecretKey", Host = "newExample.com" }; @@ -61,61 +55,7 @@ public void ShouldUpdateTwoFactorProvider_WhenExistingProviderExists() Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo)); Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientId"]); Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientSecret"]); - Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["IKey"]); - Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["SKey"]); Assert.Equal("newExample.com", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["Host"]); Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled); } - - [Fact] - public void DuoV2ParamsSync_WhenExistingProviderDoesNotExist() - { - // Arrange - var existingOrg = new Organization(); - var model = new UpdateTwoFactorDuoRequestModel - { - IntegrationKey = "integrationKey", - SecretKey = "secretKey", - Host = "example.com" - }; - - // Act - var result = model.ToOrganization(existingOrg); - - // Assert - // IKey and SKey should be the same as ClientId and ClientSecret - Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo)); - Assert.Equal("integrationKey", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientId"]); - Assert.Equal("secretKey", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientSecret"]); - Assert.Equal("integrationKey", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["IKey"]); - Assert.Equal("secretKey", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["SKey"]); - Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["Host"]); - Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled); - } - - [Fact] - public void DuoV4ParamsSync_WhenExistingProviderDoesNotExist() - { - // Arrange - var existingOrg = new Organization(); - var model = new UpdateTwoFactorDuoRequestModel - { - ClientId = "clientId", - ClientSecret = "clientSecret", - Host = "example.com" - }; - - // Act - var result = model.ToOrganization(existingOrg); - - // Assert - // IKey and SKey should be the same as ClientId and ClientSecret - Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo)); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientId"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientSecret"]); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["IKey"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["SKey"]); - Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["Host"]); - Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled); - } } diff --git a/test/Api.Test/Auth/Models/Request/TwoFactorDuoRequestModelValidationTests.cs b/test/Api.Test/Auth/Models/Request/TwoFactorDuoRequestModelValidationTests.cs index ab05a94f13fd..295d7cbb5afa 100644 --- a/test/Api.Test/Auth/Models/Request/TwoFactorDuoRequestModelValidationTests.cs +++ b/test/Api.Test/Auth/Models/Request/TwoFactorDuoRequestModelValidationTests.cs @@ -39,12 +39,9 @@ public void ShouldReturnValidationError_WhenValuesAreInvalid() var result = model.Validate(new ValidationContext(model)); // Assert - Assert.Single(result); - Assert.Equal("Neither v2 or v4 values are valid.", result.First().ErrorMessage); - Assert.Contains("ClientId", result.First().MemberNames); - Assert.Contains("ClientSecret", result.First().MemberNames); - Assert.Contains("IntegrationKey", result.First().MemberNames); - Assert.Contains("SecretKey", result.First().MemberNames); + Assert.NotEmpty(result); + Assert.True(result.Select(x => x.MemberNames.Contains("ClientId")).Any()); + Assert.True(result.Select(x => x.MemberNames.Contains("ClientSecret")).Any()); } [Fact] diff --git a/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs b/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs index 28dfc83a2de7..56c9af1e0d7e 100644 --- a/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs +++ b/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs @@ -17,8 +17,6 @@ public void ShouldAddOrUpdateTwoFactorProvider_WhenExistingProviderDoesNotExist( { ClientId = "clientId", ClientSecret = "clientSecret", - IntegrationKey = "integrationKey", - SecretKey = "secretKey", Host = "example.com" }; @@ -26,12 +24,9 @@ public void ShouldAddOrUpdateTwoFactorProvider_WhenExistingProviderDoesNotExist( var result = model.ToUser(existingUser); // Assert - // IKey and SKey should be the same as ClientId and ClientSecret Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo)); Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]); Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["IKey"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["SKey"]); Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["Host"]); Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled); } @@ -49,8 +44,6 @@ public void ShouldUpdateTwoFactorProvider_WhenExistingProviderExists() { ClientId = "newClientId", ClientSecret = "newClientSecret", - IntegrationKey = "newIntegrationKey", - SecretKey = "newSecretKey", Host = "newExample.com" }; @@ -58,65 +51,10 @@ public void ShouldUpdateTwoFactorProvider_WhenExistingProviderExists() var result = model.ToUser(existingUser); // Assert - // IKey and SKey should be the same as ClientId and ClientSecret Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo)); Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]); Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]); - Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["IKey"]); - Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["SKey"]); Assert.Equal("newExample.com", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["Host"]); Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled); } - - [Fact] - public void DuoV2ParamsSync_WhenExistingProviderDoesNotExist() - { - // Arrange - var existingUser = new User(); - var model = new UpdateTwoFactorDuoRequestModel - { - IntegrationKey = "integrationKey", - SecretKey = "secretKey", - Host = "example.com" - }; - - // Act - var result = model.ToUser(existingUser); - - // Assert - // IKey and SKey should be the same as ClientId and ClientSecret - Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo)); - Assert.Equal("integrationKey", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]); - Assert.Equal("secretKey", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]); - Assert.Equal("integrationKey", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["IKey"]); - Assert.Equal("secretKey", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["SKey"]); - Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["Host"]); - Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled); - } - - [Fact] - public void DuoV4ParamsSync_WhenExistingProviderDoesNotExist() - { - // Arrange - var existingUser = new User(); - var model = new UpdateTwoFactorDuoRequestModel - { - ClientId = "clientId", - ClientSecret = "clientSecret", - Host = "example.com" - }; - - // Act - var result = model.ToUser(existingUser); - - // Assert - // IKey and SKey should be the same as ClientId and ClientSecret - Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo)); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["IKey"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["SKey"]); - Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["Host"]); - Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled); - } } diff --git a/test/Api.Test/Auth/Models/Response/OrganizationTwoFactorDuoResponseModelTests.cs b/test/Api.Test/Auth/Models/Response/OrganizationTwoFactorDuoResponseModelTests.cs index dea76b2cdbc8..dfe27467dd1d 100644 --- a/test/Api.Test/Auth/Models/Response/OrganizationTwoFactorDuoResponseModelTests.cs +++ b/test/Api.Test/Auth/Models/Response/OrganizationTwoFactorDuoResponseModelTests.cs @@ -8,42 +8,6 @@ namespace Bit.Api.Test.Auth.Models.Response; public class OrganizationTwoFactorDuoResponseModelTests { - [Theory] - [BitAutoData] - public void Organization_WithDuoV4_ShouldBuildModel(Organization organization) - { - // Arrange - organization.TwoFactorProviders = GetTwoFactorOrganizationDuoV4ProvidersJson(); - - // Act - var model = new TwoFactorDuoResponseModel(organization); - - // Assert if v4 data Ikey and Skey are set to clientId and clientSecret - Assert.NotNull(model); - Assert.Equal("clientId", model.ClientId); - Assert.Equal("secret************", model.ClientSecret); - Assert.Equal("clientId", model.IntegrationKey); - Assert.Equal("secret************", model.SecretKey); - } - - [Theory] - [BitAutoData] - public void Organization_WithDuoV2_ShouldBuildModel(Organization organization) - { - // Arrange - organization.TwoFactorProviders = GetTwoFactorOrganizationDuoV2ProvidersJson(); - - // Act - var model = new TwoFactorDuoResponseModel(organization); - - // Assert if only v2 data clientId and clientSecret are set to Ikey and Sk - Assert.NotNull(model); - Assert.Equal("IKey", model.ClientId); - Assert.Equal("SKey", model.ClientSecret); - Assert.Equal("IKey", model.IntegrationKey); - Assert.Equal("SKey", model.SecretKey); - } - [Theory] [BitAutoData] public void Organization_WithDuo_ShouldBuildModel(Organization organization) @@ -54,12 +18,10 @@ public void Organization_WithDuo_ShouldBuildModel(Organization organization) // Act var model = new TwoFactorDuoResponseModel(organization); - /// Assert Even if both versions are present priority is given to v4 data + // Assert Assert.NotNull(model); Assert.Equal("clientId", model.ClientId); Assert.Equal("secret************", model.ClientSecret); - Assert.Equal("clientId", model.IntegrationKey); - Assert.Equal("secret************", model.SecretKey); } [Theory] @@ -72,38 +34,33 @@ public void Organization_WithDuoEmpty_ShouldFail(Organization organization) // Act var model = new TwoFactorDuoResponseModel(organization); - /// Assert + // Assert Assert.False(model.Enabled); } [Theory] [BitAutoData] - public void Organization_WithTwoFactorProvidersNull_ShouldFail(Organization organization) + public void Organization_WithTwoFactorProvidersNull_ShouldThrow(Organization organization) { // Arrange - organization.TwoFactorProviders = "{\"6\" : {}}"; + organization.TwoFactorProviders = null; // Act - var model = new TwoFactorDuoResponseModel(organization); - - /// Assert - Assert.False(model.Enabled); + try + { + var model = new TwoFactorDuoResponseModel(organization); + + } + catch (Exception ex) + { + // Assert + Assert.IsType(ex); + } } private string GetTwoFactorOrganizationDuoProvidersJson() - { - return - "{\"6\":{\"Enabled\":true,\"MetaData\":{\"SKey\":\"SKey\",\"IKey\":\"IKey\",\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; - } - - private string GetTwoFactorOrganizationDuoV4ProvidersJson() { return "{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; } - - private string GetTwoFactorOrganizationDuoV2ProvidersJson() - { - return "{\"6\":{\"Enabled\":true,\"MetaData\":{\"SKey\":\"SKey\",\"IKey\":\"IKey\",\"Host\":\"example.com\"}}}"; - } } diff --git a/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs b/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs index cb46273a60d2..2441f104b34f 100644 --- a/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs +++ b/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs @@ -10,38 +10,21 @@ public class UserTwoFactorDuoResponseModelTests { [Theory] [BitAutoData] - public void User_WithDuoV4_ShouldBuildModel(User user) + public void User_WithDuo_UserNull_ThrowsArgumentException(User user) { // Arrange - user.TwoFactorProviders = GetTwoFactorDuoV4ProvidersJson(); + user.TwoFactorProviders = GetTwoFactorDuoProvidersJson(); // Act - var model = new TwoFactorDuoResponseModel(user); - - // Assert if v4 data Ikey and Skey are set to clientId and clientSecret - Assert.NotNull(model); - Assert.Equal("clientId", model.ClientId); - Assert.Equal("secret************", model.ClientSecret); - Assert.Equal("clientId", model.IntegrationKey); - Assert.Equal("secret************", model.SecretKey); - } - - [Theory] - [BitAutoData] - public void User_WithDuov2_ShouldBuildModel(User user) - { - // Arrange - user.TwoFactorProviders = GetTwoFactorDuoV2ProvidersJson(); - - // Act - var model = new TwoFactorDuoResponseModel(user); - - // Assert if only v2 data clientId and clientSecret are set to Ikey and Skey - Assert.NotNull(model); - Assert.Equal("IKey", model.ClientId); - Assert.Equal("SKey", model.ClientSecret); - Assert.Equal("IKey", model.IntegrationKey); - Assert.Equal("SKey", model.SecretKey); + try + { + var model = new TwoFactorDuoResponseModel(null as User); + } + catch (ArgumentNullException e) + { + // Assert + Assert.Equal("Value cannot be null. (Parameter 'user')", e.Message); + } } [Theory] @@ -54,12 +37,10 @@ public void User_WithDuo_ShouldBuildModel(User user) // Act var model = new TwoFactorDuoResponseModel(user); - // Assert Even if both versions are present priority is given to v4 data + // Assert Assert.NotNull(model); Assert.Equal("clientId", model.ClientId); Assert.Equal("secret************", model.ClientSecret); - Assert.Equal("clientId", model.IntegrationKey); - Assert.Equal("secret************", model.SecretKey); } [Theory] @@ -84,26 +65,23 @@ public void User_WithTwoFactorProvidersNull_ShouldFail(User user) user.TwoFactorProviders = null; // Act - var model = new TwoFactorDuoResponseModel(user); + try + { + var model = new TwoFactorDuoResponseModel(user); - /// Assert - Assert.False(model.Enabled); - } + } + catch (Exception ex) + { + // Assert + Assert.IsType(ex); + + } - private string GetTwoFactorDuoProvidersJson() - { - return - "{\"2\":{\"Enabled\":true,\"MetaData\":{\"SKey\":\"SKey\",\"IKey\":\"IKey\",\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; } - private string GetTwoFactorDuoV4ProvidersJson() + private string GetTwoFactorDuoProvidersJson() { return "{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; } - - private string GetTwoFactorDuoV2ProvidersJson() - { - return "{\"2\":{\"Enabled\":true,\"MetaData\":{\"SKey\":\"SKey\",\"IKey\":\"IKey\",\"Host\":\"example.com\"}}}"; - } } diff --git a/test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs b/test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs index 105a6632d893..e8a88c684838 100644 --- a/test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs +++ b/test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs @@ -13,8 +13,8 @@ namespace Bit.Test.Common.AutoFixture.Attributes; public abstract class BitCustomizeAttribute : Attribute { /// - /// /// Gets a customization for the method's parameters. + /// Gets a customization for the method's parameters. /// - /// A customization for the method's paramters. + /// A customization for the method's parameters. public abstract ICustomization GetCustomization(); } diff --git a/test/Core.Test/Auth/Identity/AuthenticationTokenProviderTests.cs b/test/Core.Test/Auth/Identity/AuthenticationTokenProviderTests.cs index a76216f3fab7..c9646e627c00 100644 --- a/test/Core.Test/Auth/Identity/AuthenticationTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/AuthenticationTokenProviderTests.cs @@ -1,5 +1,5 @@ using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Identity; +using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Entities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs b/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs index b90f71ae71bc..da2d4a282adb 100644 --- a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs @@ -19,7 +19,6 @@ public abstract class BaseTokenProviderTests { public abstract TwoFactorProviderType TwoFactorProviderType { get; } - #region Helpers protected static IEnumerable SetupCanGenerateData(params (Dictionary MetaData, bool ExpectedResponse)[] data) { return data.Select(d => @@ -48,6 +47,9 @@ protected virtual void SetupUserService(IUserService userService, User user) userService .TwoFactorProviderIsEnabledAsync(TwoFactorProviderType, user) .Returns(true); + userService + .CanAccessPremium(user) + .Returns(true); } protected static UserManager SubstituteUserManager() @@ -76,7 +78,6 @@ protected void MockDatabase(User user, Dictionary metaData) user.TwoFactorProviders = JsonHelpers.LegacySerialize(providers); } - #endregion public virtual async Task RunCanGenerateTwoFactorTokenAsync(Dictionary metaData, bool expectedResponse, User user, SutProvider sutProvider) diff --git a/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs b/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs new file mode 100644 index 000000000000..85c687119b8e --- /dev/null +++ b/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs @@ -0,0 +1,262 @@ +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity.TokenProviders; +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Entities; +using Bit.Core.Tokens; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; +using Duo = DuoUniversal; + +namespace Bit.Core.Test.Auth.Identity; + +public class DuoUniversalTwoFactorTokenProviderTests : BaseTokenProviderTests +{ + private readonly IDuoUniversalTokenService _duoUniversalTokenService = Substitute.For(); + public override TwoFactorProviderType TwoFactorProviderType => TwoFactorProviderType.Duo; + + public static IEnumerable CanGenerateTwoFactorTokenAsyncData + => SetupCanGenerateData( + ( // correct data + new Dictionary + { + ["ClientId"] = new string('c', 20), + ["ClientSecret"] = new string('s', 40), + ["Host"] = "https://api-abcd1234.duosecurity.com", + }, + true + ), + ( // correct data duo federal + new Dictionary + { + ["ClientId"] = new string('c', 20), + ["ClientSecret"] = new string('s', 40), + ["Host"] = "https://api-abcd1234.duofederal.com", + }, + true + ), + ( // correct data duo federal + new Dictionary + { + ["ClientId"] = new string('c', 20), + ["ClientSecret"] = new string('s', 40), + ["Host"] = "https://api-abcd1234.duofederal.com", + }, + true + ), + ( // invalid host + new Dictionary + { + ["ClientId"] = new string('c', 20), + ["ClientSecret"] = new string('s', 40), + ["Host"] = "", + }, + false + ), + ( // clientId missing + new Dictionary + { + ["ClientSecret"] = new string('s', 40), + ["Host"] = "https://api-abcd1234.duofederal.com", + }, + false + ) + ); + + public static IEnumerable NonPremiumCanGenerateTwoFactorTokenAsyncData + => SetupCanGenerateData( + ( // correct data + new Dictionary + { + ["ClientId"] = new string('c', 20), + ["ClientSecret"] = new string('s', 40), + ["Host"] = "https://api-abcd1234.duosecurity.com", + }, + false + ) + ); + + [Theory, BitMemberAutoData(nameof(CanGenerateTwoFactorTokenAsyncData))] + public override async Task RunCanGenerateTwoFactorTokenAsync(Dictionary metaData, bool expectedResponse, + User user, SutProvider sutProvider) + { + // Arrange + user.Premium = true; + user.PremiumExpirationDate = DateTime.UtcNow.AddDays(1); + + sutProvider.GetDependency() + .HasProperDuoMetadata(Arg.Any()) + .Returns(expectedResponse); + + // Act + // Assert + await base.RunCanGenerateTwoFactorTokenAsync(metaData, expectedResponse, user, sutProvider); + } + + [Theory, BitMemberAutoData(nameof(NonPremiumCanGenerateTwoFactorTokenAsyncData))] + public async Task CanGenerateTwoFactorTokenAsync_UserCanNotAccessPremium_ReturnsNull(Dictionary metaData, bool expectedResponse, + User user, SutProvider sutProvider) + { + // Arrange + user.Premium = false; + + sutProvider.GetDependency() + .HasProperDuoMetadata(Arg.Any()) + .Returns(expectedResponse); + + // Act + // Assert + await base.RunCanGenerateTwoFactorTokenAsync(metaData, expectedResponse, user, sutProvider); + } + + [Theory] + [BitAutoData] + public async Task GenerateToken_Success_ReturnsAuthUrl( + User user, SutProvider sutProvider, string authUrl) + { + // Arrange + SetUpProperDuoUniversalTokenService(user, sutProvider); + + sutProvider.GetDependency() + .GenerateAuthUrl( + Arg.Any(), + Arg.Any>(), + user) + .Returns(authUrl); + + // Act + var token = await sutProvider.Sut.GenerateAsync("purpose", SubstituteUserManager(), user); + + // Assert + Assert.NotNull(token); + Assert.Equal(token, authUrl); + } + + [Theory] + [BitAutoData] + public async Task GenerateToken_DuoClientNull_ReturnsNull( + User user, SutProvider sutProvider) + { + // Arrange + user.Premium = true; + user.TwoFactorProviders = GetTwoFactorDuoProvidersJson(); + AdditionalSetup(sutProvider, user); + + sutProvider.GetDependency() + .HasProperDuoMetadata(Arg.Any()) + .Returns(true); + + sutProvider.GetDependency() + .BuildDuoTwoFactorClientAsync(Arg.Any()) + .Returns(null as Duo.Client); + + // Act + var token = await sutProvider.Sut.GenerateAsync("purpose", SubstituteUserManager(), user); + + // Assert + Assert.Null(token); + } + + [Theory] + [BitAutoData] + public async Task GenerateToken_UserCanNotAccessPremium_ReturnsNull( + User user, SutProvider sutProvider) + { + // Arrange + user.Premium = false; + user.TwoFactorProviders = GetTwoFactorDuoProvidersJson(); + AdditionalSetup(sutProvider, user); + + // Act + var token = await sutProvider.Sut.GenerateAsync("purpose", SubstituteUserManager(), user); + + // Assert + Assert.Null(token); + } + + [Theory] + [BitAutoData] + public async Task ValidateToken_ValidToken_ReturnsTrue( + User user, SutProvider sutProvider, string token) + { + // Arrange + SetUpProperDuoUniversalTokenService(user, sutProvider); + + sutProvider.GetDependency() + .RequestDuoValidationAsync( + Arg.Any(), + Arg.Any>(), + user, + token) + .Returns(true); + + // Act + var response = await sutProvider.Sut.ValidateAsync("purpose", token, SubstituteUserManager(), user); + + // Assert + Assert.True(response); + } + + [Theory] + [BitAutoData] + public async Task ValidateToken_DuoClientNull_ReturnsFalse( + User user, SutProvider sutProvider, string token) + { + user.Premium = true; + user.TwoFactorProviders = GetTwoFactorDuoProvidersJson(); + AdditionalSetup(sutProvider, user); + + sutProvider.GetDependency() + .HasProperDuoMetadata(Arg.Any()) + .Returns(true); + + sutProvider.GetDependency() + .BuildDuoTwoFactorClientAsync(Arg.Any()) + .Returns(null as Duo.Client); + + // Act + var result = await sutProvider.Sut.ValidateAsync("purpose", token, SubstituteUserManager(), user); + + // Assert + Assert.False(result); + } + + /// + /// Ensures that the IDuoUniversalTokenService is properly setup for the test. + /// This ensures that the private GetDuoClientAsync, and GetDuoTwoFactorProvider + /// methods will return true enabling the test to execute on the correct path. + /// + /// user from calling test + /// self + private void SetUpProperDuoUniversalTokenService(User user, SutProvider sutProvider) + { + user.Premium = true; + user.TwoFactorProviders = GetTwoFactorDuoProvidersJson(); + var client = BuildDuoClient(); + + AdditionalSetup(sutProvider, user); + + sutProvider.GetDependency() + .HasProperDuoMetadata(Arg.Any()) + .Returns(true); + + sutProvider.GetDependency() + .BuildDuoTwoFactorClientAsync(Arg.Any()) + .Returns(client); + } + + private Duo.Client BuildDuoClient() + { + var clientId = new string('c', 20); + var clientSecret = new string('s', 40); + return new Duo.ClientBuilder(clientId, clientSecret, "api-abcd1234.duosecurity.com", "redirectUrl").Build(); + } + + private string GetTwoFactorDuoProvidersJson() + { + return + "{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; + } +} diff --git a/test/Core.Test/Auth/Identity/EmailTwoFactorTokenProviderTests.cs b/test/Core.Test/Auth/Identity/EmailTwoFactorTokenProviderTests.cs index c5855c2343a4..46bfba549e80 100644 --- a/test/Core.Test/Auth/Identity/EmailTwoFactorTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/EmailTwoFactorTokenProviderTests.cs @@ -1,5 +1,5 @@ using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Identity; +using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Entities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Core.Test/Auth/Identity/OrganizationDuoUniversalTwoFactorTokenProviderTests.cs b/test/Core.Test/Auth/Identity/OrganizationDuoUniversalTwoFactorTokenProviderTests.cs new file mode 100644 index 000000000000..ed67d89f9d57 --- /dev/null +++ b/test/Core.Test/Auth/Identity/OrganizationDuoUniversalTwoFactorTokenProviderTests.cs @@ -0,0 +1,289 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Identity.TokenProviders; +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Entities; +using Bit.Core.Tokens; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; +using Duo = DuoUniversal; + +namespace Bit.Core.Test.Auth.Identity; + +[SutProviderCustomize] +public class OrganizationDuoUniversalTwoFactorTokenProviderTests +{ + private readonly IDuoUniversalTokenService _duoUniversalTokenService = Substitute.For(); + private readonly IDataProtectorTokenFactory _tokenDataFactory = Substitute.For>(); + + // Happy path + [Theory] + [BitAutoData] + public async Task CanGenerateTwoFactorTokenAsync_ReturnsTrue( + Organization organization, SutProvider sutProvider) + { + // Arrange + organization.Enabled = true; + organization.Use2fa = true; + SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider); + + // Act + var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(organization); + + // Assert + Assert.True(result); + } + + [Theory] + [BitAutoData] + public async Task CanGenerateTwoFactorTokenAsync_DuoTwoFactorNotEnabled_ReturnsFalse( + Organization organization, SutProvider sutProvider) + { + // Arrange + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderNotEnabledJson(); + organization.Use2fa = true; + organization.Enabled = true; + + sutProvider.GetDependency() + .HasProperDuoMetadata(Arg.Any()) + .Returns(true); + // Act + var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(null); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async Task CanGenerateTwoFactorTokenAsync_BadMetaData_ProviderNull_ReturnsFalse( + Organization organization, SutProvider sutProvider) + { + // Arrange + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); + organization.Use2fa = true; + organization.Enabled = true; + + sutProvider.GetDependency() + .HasProperDuoMetadata(Arg.Any()) + .Returns(false); + // Act + var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(null); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async Task GetDuoTwoFactorProvider_OrganizationNull_ReturnsNull( + SutProvider sutProvider) + { + // Act + var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(null); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async Task GetDuoTwoFactorProvider_OrganizationNotEnabled_ReturnsNull( + Organization organization, SutProvider sutProvider) + { + // Arrange + SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider); + organization.Enabled = false; + + // Act + var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(organization); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async Task GetDuoTwoFactorProvider_OrganizationUse2FAFalse_ReturnsNull( + Organization organization, SutProvider sutProvider) + { + // Arrange + SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider); + organization.Use2fa = false; + + // Act + var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(organization); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async Task GetDuoClient_ProviderNull_ReturnsNull( + SutProvider sutProvider) + { + // Act + var result = await sutProvider.Sut.GenerateAsync(null, default); + + // Assert + Assert.Null(result); + } + + [Theory] + [BitAutoData] + public async Task GetDuoClient_DuoClientNull_ReturnsNull( + SutProvider sutProvider, + Organization organization) + { + // Arrange + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); + organization.Use2fa = true; + organization.Enabled = true; + + sutProvider.GetDependency() + .HasProperDuoMetadata(Arg.Any()) + .Returns(true); + + sutProvider.GetDependency() + .BuildDuoTwoFactorClientAsync(Arg.Any()) + .Returns(null as Duo.Client); + + // Act + var result = await sutProvider.Sut.GenerateAsync(organization, default); + + // Assert + Assert.Null(result); + } + + [Theory] + [BitAutoData] + public async Task GenerateAsync_ReturnsAuthUrl( + SutProvider sutProvider, + Organization organization, + User user, + string AuthUrl) + { + // Arrange + SetUpProperOrganizationDuoUniversalTokenService(BuildDuoClient(), organization, sutProvider); + + sutProvider.GetDependency() + .GenerateAuthUrl(Arg.Any(), Arg.Any>(), user) + .Returns(AuthUrl); + + // Act + var result = await sutProvider.Sut.GenerateAsync(organization, user); + + // Assert + Assert.NotNull(result); + Assert.Equal(AuthUrl, result); + } + + [Theory] + [BitAutoData] + public async Task GenerateAsync_ClientNull_ReturnsNull( + SutProvider sutProvider, + Organization organization, + User user) + { + // Arrange + SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider); + + // Act + var result = await sutProvider.Sut.GenerateAsync(organization, user); + + // Assert + Assert.Null(result); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_TokenValid_ReturnsTrue( + SutProvider sutProvider, + Organization organization, + User user, + string token) + { + // Arrange + SetUpProperOrganizationDuoUniversalTokenService(BuildDuoClient(), organization, sutProvider); + + sutProvider.GetDependency() + .RequestDuoValidationAsync(Arg.Any(), Arg.Any>(), user, token) + .Returns(true); + + // Act + var result = await sutProvider.Sut.ValidateAsync(token, organization, user); + + // Assert + Assert.True(result); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_ClientNull_ReturnsFalse( + SutProvider sutProvider, + Organization organization, + User user, + string token) + { + // Arrange + SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider); + + sutProvider.GetDependency() + .RequestDuoValidationAsync(Arg.Any(), Arg.Any>(), user, token) + .Returns(true); + + // Act + var result = await sutProvider.Sut.ValidateAsync(token, organization, user); + + // Assert + Assert.False(result); + } + + /// + /// Ensures that the IDuoUniversalTokenService is properly setup for the test. + /// This ensures that the private GetDuoClientAsync, and GetDuoTwoFactorProvider + /// methods will return true enabling the test to execute on the correct path. + /// + /// BitAutoData cannot create the Duo.Client since it does not have a public constructor + /// so we have to use the ClientBUilder(), the client is not used meaningfully in the tests. + /// + /// user from calling test + /// self + private void SetUpProperOrganizationDuoUniversalTokenService( + Duo.Client client, Organization organization, SutProvider sutProvider) + { + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); + organization.Enabled = true; + organization.Use2fa = true; + + sutProvider.GetDependency() + .HasProperDuoMetadata(Arg.Any()) + .Returns(true); + + sutProvider.GetDependency() + .BuildDuoTwoFactorClientAsync(Arg.Any()) + .Returns(client); + } + + private Duo.Client BuildDuoClient() + { + var clientId = new string('c', 20); + var clientSecret = new string('s', 40); + return new Duo.ClientBuilder(clientId, clientSecret, "api-abcd1234.duosecurity.com", "redirectUrl").Build(); + } + + private string GetTwoFactorOrganizationDuoProviderJson() + { + return + "{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; + } + + private string GetTwoFactorOrganizationDuoProviderNotEnabledJson() + { + return + "{\"6\":{\"Enabled\":false,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; + } +} diff --git a/test/Core.Test/Auth/Services/DuoUniversalTokenServiceTests.cs b/test/Core.Test/Auth/Services/DuoUniversalTokenServiceTests.cs new file mode 100644 index 000000000000..4d205dc44b4c --- /dev/null +++ b/test/Core.Test/Auth/Services/DuoUniversalTokenServiceTests.cs @@ -0,0 +1,91 @@ +using Bit.Core.Auth.Identity.TokenProviders; +using Bit.Core.Auth.Models; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.Auth.Services; + +[SutProviderCustomize] +public class DuoUniversalTokenServiceTests +{ + [Theory] + [BitAutoData("", "ClientId", "ClientSecret")] + [BitAutoData("api-valid.duosecurity.com", "", "ClientSecret")] + [BitAutoData("api-valid.duosecurity.com", "ClientId", "")] + public async void ValidateDuoConfiguration_InvalidConfig_ReturnsFalse( + string host, string clientId, string clientSecret, SutProvider sutProvider) + { + // Arrange + /* AutoData handles arrangement */ + + // Act + var result = await sutProvider.Sut.ValidateDuoConfiguration(clientSecret, clientId, host); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData(true, "api-valid.duosecurity.com")] + [BitAutoData(false, "invalid")] + [BitAutoData(false, "api-valid.duosecurity.com", null, "clientSecret")] + [BitAutoData(false, "api-valid.duosecurity.com", "ClientId", null)] + [BitAutoData(false, "api-valid.duosecurity.com", null, null)] + public void HasProperDuoMetadata_ReturnMatchesExpected( + bool expectedResponse, string host, string clientId, + string clientSecret, SutProvider sutProvider) + { + // Arrange + var metaData = new Dictionary { ["Host"] = host }; + + if (clientId != null) + { + metaData.Add("ClientId", clientId); + } + + if (clientSecret != null) + { + metaData.Add("ClientSecret", clientSecret); + } + + var provider = new TwoFactorProvider + { + MetaData = metaData + }; + + // Act + var result = sutProvider.Sut.HasProperDuoMetadata(provider); + + // Assert + Assert.Equal(result, expectedResponse); + } + + [Theory] + [BitAutoData] + public void HasProperDuoMetadata_ProviderIsNull_ReturnsFalse( + SutProvider sutProvider) + { + // Act + var result = sutProvider.Sut.HasProperDuoMetadata(null); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData("api-valid.duosecurity.com", true)] + [BitAutoData("api-valid.duofederal.com", true)] + [BitAutoData("invalid", false)] + public void ValidDuoHost_HostIsValid_ReturnTrue( + string host, bool expectedResponse) + { + // Act + var result = DuoUniversalTokenService.ValidDuoHost(host); + + // Assert + Assert.Equal(result, expectedResponse); + } + + +} diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs index 468ef5a169ba..4e598c436d6f 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs @@ -28,7 +28,7 @@ namespace Bit.Identity.IntegrationTest.Endpoints; public class IdentityServerTwoFactorTests : IClassFixture { - const string _organizationTwoFactor = """{"6":{"Enabled":true,"MetaData":{"IKey":"DIEFB13LB49IEB3459N2","SKey":"0ZnsZHav0KcNPBZTS6EOUwqLPoB0sfMd5aJeWExQ","Host":"api-example.duosecurity.com"}}}"""; + const string _organizationTwoFactor = """{"6":{"Enabled":true,"MetaData":{"ClientId":"DIEFB13LB49IEB3459N2","ClientSecret":"0ZnsZHav0KcNPBZTS6EOUwqLPoB0sfMd5aJeWExQ","Host":"api-example.duosecurity.com"}}}"""; const string _testEmail = "test+2farequired@email.com"; const string _testPassword = "master_password_hash"; const string _userEmailTwoFactor = """{"1": { "Enabled": true, "MetaData": { "Email": "test+2farequired@email.com"}}}"""; @@ -140,7 +140,7 @@ await CreateSsoOrganizationAndUserAsync( { "password", _testPassword }, }), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); - // Assert + // Assert using var responseBody = await AssertHelper.AssertResponseTypeIs(context); var root = responseBody.RootElement; var error = AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String).GetString(); @@ -168,7 +168,7 @@ public async Task TokenEndpoint_GrantTypePassword_RememberTwoFactorType_InvalidT [Theory, BitAutoData] public async Task TokenEndpoint_GrantTypeClientCredential_OrgTwoFactorRequired_Success(Organization organization, OrganizationApiKey organizationApiKey) { - // Arrange + // Arrange organization.Enabled = true; organization.UseApi = true; organization.Use2fa = true; @@ -258,7 +258,7 @@ await CreateSsoOrganizationAndUserAsync( { "redirect_uri", "https://localhost:8080/sso-connector.html" } }), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); - // Assert + // Assert using var responseBody = await AssertHelper.AssertResponseTypeIs(context); var root = responseBody.RootElement; var error = AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String).GetString(); @@ -320,7 +320,7 @@ await CreateSsoOrganizationAndUserAsync( }), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); - // Assert + // Assert var body = await AssertHelper.AssertResponseTypeIs(twoFactorProvidedContext); var root = body.RootElement; @@ -338,6 +338,7 @@ public async Task TokenEndpoint_GrantTypeAuthCode_OrgTwoFactorRequired_OrgDuoTwo { MemberDecryptionType = MemberDecryptionType.MasterPassword, }; + await CreateSsoOrganizationAndUserAsync( localFactory, ssoConfigData, challenge, _testEmail, orgTwoFactor: _organizationTwoFactor); @@ -355,7 +356,7 @@ await CreateSsoOrganizationAndUserAsync( { "redirect_uri", "https://localhost:8080/sso-connector.html" } }), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); - // Assert + // Assert using var responseBody = await AssertHelper.AssertResponseTypeIs(context); var root = responseBody.RootElement; var error = AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String).GetString(); diff --git a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs index 5783375ff788..dfb877b8d680 100644 --- a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs @@ -1,8 +1,6 @@ -using Bit.Core; -using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Identity; -using Bit.Core.Auth.Models; +using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Context; using Bit.Core.Entities; @@ -28,8 +26,7 @@ public class TwoFactorAuthenticationValidatorTests { private readonly IUserService _userService; private readonly UserManagerTestWrapper _userManager; - private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; - private readonly ITemporaryDuoWebV4SDKService _temporaryDuoWebV4SDKService; + private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider; private readonly IFeatureService _featureService; private readonly IApplicationCacheService _applicationCacheService; private readonly IOrganizationUserRepository _organizationUserRepository; @@ -42,8 +39,7 @@ public TwoFactorAuthenticationValidatorTests() { _userService = Substitute.For(); _userManager = SubstituteUserManager(); - _organizationDuoWebTokenProvider = Substitute.For(); - _temporaryDuoWebV4SDKService = Substitute.For(); + _organizationDuoUniversalTokenProvider = Substitute.For(); _featureService = Substitute.For(); _applicationCacheService = Substitute.For(); _organizationUserRepository = Substitute.For(); @@ -54,8 +50,7 @@ public TwoFactorAuthenticationValidatorTests() _sut = new TwoFactorAuthenticationValidator( _userService, _userManager, - _organizationDuoWebTokenProvider, - _temporaryDuoWebV4SDKService, + _organizationDuoUniversalTokenProvider, _featureService, _applicationCacheService, _organizationUserRepository, @@ -439,7 +434,7 @@ public async void VerifyTwoFactorAsync_Organization_ValidToken_ReturnsTrue( string token) { // Arrange - _organizationDuoWebTokenProvider.ValidateAsync( + _organizationDuoUniversalTokenProvider.ValidateAsync( token, organization, user).Returns(true); _userManager.TWO_FACTOR_ENABLED = true; @@ -457,70 +452,6 @@ public async void VerifyTwoFactorAsync_Organization_ValidToken_ReturnsTrue( Assert.True(result); } - [Theory] - [BitAutoData(TwoFactorProviderType.Duo)] - [BitAutoData(TwoFactorProviderType.OrganizationDuo)] - public async void VerifyTwoFactorAsync_TemporaryDuoService_ValidToken_ReturnsTrue( - TwoFactorProviderType providerType, - User user, - Organization organization, - string token) - { - // Arrange - _featureService.IsEnabled(FeatureFlagKeys.DuoRedirect).Returns(true); - _userService.TwoFactorProviderIsEnabledAsync(providerType, user).Returns(true); - _temporaryDuoWebV4SDKService.ValidateAsync( - token, Arg.Any(), user).Returns(true); - - user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); - organization.Use2fa = true; - organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); - organization.Enabled = true; - - _userManager.TWO_FACTOR_ENABLED = true; - _userManager.TWO_FACTOR_TOKEN = token; - _userManager.TWO_FACTOR_TOKEN_VERIFIED = true; - - // Act - var result = await _sut.VerifyTwoFactor( - user, organization, providerType, token); - - // Assert - Assert.True(result); - } - - [Theory] - [BitAutoData(TwoFactorProviderType.Duo)] - [BitAutoData(TwoFactorProviderType.OrganizationDuo)] - public async void VerifyTwoFactorAsync_TemporaryDuoService_InvalidToken_ReturnsFalse( - TwoFactorProviderType providerType, - User user, - Organization organization, - string token) - { - // Arrange - _featureService.IsEnabled(FeatureFlagKeys.DuoRedirect).Returns(true); - _userService.TwoFactorProviderIsEnabledAsync(providerType, user).Returns(true); - _temporaryDuoWebV4SDKService.ValidateAsync( - token, Arg.Any(), user).Returns(true); - - user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); - organization.Use2fa = true; - organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); - organization.Enabled = true; - - _userManager.TWO_FACTOR_ENABLED = true; - _userManager.TWO_FACTOR_TOKEN = token; - _userManager.TWO_FACTOR_TOKEN_VERIFIED = false; - - // Act - var result = await _sut.VerifyTwoFactor( - user, organization, providerType, token); - - // Assert - Assert.True(result); - } - private static UserManagerTestWrapper SubstituteUserManager() { return new UserManagerTestWrapper( From b2b0f1e70ea2a54d183c24684be6b640e1a81b02 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:04:54 +0000 Subject: [PATCH 13/16] [deps] Auth: Update bootstrap to v5 [SECURITY] (#4881) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [deps] Auth: Update bootstrap to v5 [SECURITY] * Update bootstrap and import dependencies in site.scss * Update site.scss to include the theme color 'dark' * Refactor site.scss to merge the 'primary-accent' theme color into the existing theme colors * Update bootstrap classes for v5 * Refactor form layout in Index.cshtml and AddExistingOrganization.cshtml * Revert change to the shield icon in the navbar * Fix organization form select inputs * Fixed search input sizes * Fix elements in Providers and Users search * More bootstrap migration * Revert change to tax rate delete button * Add missing label classes in Users/Edit.cshtml * More component migrations * Refactor form classes and labels in CreateMsp.cshtml and CreateReseller.cshtml * Update package dependencies in Sso * Revert changes to Providers/Edit.cshtml * Refactor CreateMultiOrganizationEnterprise.cshtml and Providers/Edit.cshtml for bootstrap 5 * Refactor webpack.config.js to use @popperjs/core instead of popper.js * Remove popperjs package dependency * Restore Bootstrap 4 link styling behavior - Remove default text decoration - Add underline only on hover * Update Bootstrap to version 5.3.3 * Update deprecated text color classes from 'text-muted' to 'text-body-secondary' across various views * Refactor provider edit view for bootstrap 5 * Remove underline in Add/Create organization links in provider page --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Rui Tome Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> --- .../src/Sso/Views/Shared/_Layout.cshtml | 2 +- bitwarden_license/src/Sso/package-lock.json | 37 +- bitwarden_license/src/Sso/package.json | 5 +- bitwarden_license/src/Sso/webpack.config.js | 2 - .../Views/Organizations/Edit.cshtml | 10 +- .../Views/Organizations/Index.cshtml | 42 +- .../Providers/AddExistingOrganization.cshtml | 18 +- .../Views/Providers/Create.cshtml | 10 +- .../Views/Providers/CreateMsp.cshtml | 14 +- .../CreateMultiOrganizationEnterprise.cshtml | 20 +- .../Views/Providers/CreateOrganization.cshtml | 2 +- .../Views/Providers/CreateReseller.cshtml | 14 +- .../AdminConsole/Views/Providers/Edit.cshtml | 95 +++-- .../AdminConsole/Views/Providers/Index.cshtml | 26 +- .../Views/Providers/Organizations.cshtml | 12 +- .../Views/Shared/_OrganizationForm.cshtml | 106 +++-- src/Admin/Auth/Views/Login/Index.cshtml | 10 +- .../Views/MigrateProviders/Index.cshtml | 16 +- src/Admin/Sass/site.scss | 16 +- src/Admin/Views/Home/Index.cshtml | 8 +- src/Admin/Views/Shared/_Layout.cshtml | 10 +- .../Tools/CreateUpdateTransaction.cshtml | 74 ++-- src/Admin/Views/Tools/GenerateLicense.cshtml | 16 +- src/Admin/Views/Tools/PromoteAdmin.cshtml | 10 +- .../Views/Tools/StripeSubscriptions.cshtml | 366 +++++++++--------- src/Admin/Views/Tools/TaxRate.cshtml | 16 +- src/Admin/Views/Users/Edit.cshtml | 54 ++- src/Admin/Views/Users/Index.cshtml | 22 +- src/Admin/package-lock.json | 35 +- src/Admin/package.json | 3 +- src/Admin/webpack.config.js | 2 - src/Billing/Views/Login/Index.cshtml | 2 +- 32 files changed, 535 insertions(+), 540 deletions(-) diff --git a/bitwarden_license/src/Sso/Views/Shared/_Layout.cshtml b/bitwarden_license/src/Sso/Views/Shared/_Layout.cshtml index fc57d7e9bdd0..b5330dfa92bf 100644 --- a/bitwarden_license/src/Sso/Views/Shared/_Layout.cshtml +++ b/bitwarden_license/src/Sso/Views/Shared/_Layout.cshtml @@ -23,7 +23,7 @@ @RenderBody() -