1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-24 12:35:25 +01:00

Merge branch 'main' into PM-11162-assign-to-collection-perm-update

This commit is contained in:
jng 2024-11-15 11:37:01 -05:00
commit 710b0e2695
No known key found for this signature in database
GPG Key ID: AF822623CAD19C85
48 changed files with 9741 additions and 302 deletions

View File

@ -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.Billing.Repositories;
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 class CreateProviderCommand : ICreateProviderCommand
{
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<Guid> CreateProviderAsync(Provider provider, string ownerEmail)
@ -77,12 +63,7 @@ public class CreateProviderCommand : ICreateProviderCommand
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);

View File

@ -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 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
Provider provider,
IEnumerable<string> 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

View File

@ -8,7 +8,6 @@ using Bit.Core.AdminConsole.Models.Business.Tokenables;
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 class ProviderService : IProviderService
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 class ProviderService : IProviderService
{
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 @@ public class ProviderService : IProviderService
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))

View File

@ -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 class RemoveOrganizationFromProviderCommandTests
"b@example.com"
]);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(false);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(GetSubscription(organization.GatewaySubscriptionId));
@ -222,9 +218,6 @@ public class RemoveOrganizationFromProviderCommandTests
"b@example.com"
]);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription

View File

@ -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 class ProviderServiceTests
}
[Theory, BitAutoData]
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key,
[ProviderUser(ProviderUserStatusType.Confirmed, ProviderUserType.ProviderAdmin)] ProviderUser providerUser,
SutProvider<ProviderService> sutProvider)
{
providerUser.ProviderId = provider.Id;
providerUser.UserId = user.Id;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByIdAsync(user.Id).Returns(user);
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
sutProvider.GetDependency<IDataProtectionProvider>().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<IProviderRepository>().Received().UpsertAsync(provider);
await sutProvider.GetDependency<IProviderUserRepository>().Received()
.ReplaceAsync(Arg.Is<ProviderUser>(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<ProviderService> sutProvider)
{
providerUser.ProviderId = provider.Id;
@ -100,9 +71,6 @@ public class ProviderServiceTests
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
.Returns(protector);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
var customer = new Customer { Id = "customer_id" };
@ -489,7 +457,7 @@ public class ProviderServiceTests
public async Task AddOrganization_OrganizationHasSecretsManager_Throws(Provider provider, Organization organization, string key,
SutProvider<ProviderService> sutProvider)
{
organization.PlanType = PlanType.EnterpriseAnnually;
organization.PlanType = PlanType.EnterpriseMonthly;
organization.UseSecretsManager = true;
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
@ -506,7 +474,7 @@ public class ProviderServiceTests
public async Task AddOrganization_Success(Provider provider, Organization organization, string key,
SutProvider<ProviderService> sutProvider)
{
organization.PlanType = PlanType.EnterpriseAnnually;
organization.PlanType = PlanType.EnterpriseMonthly;
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
@ -549,8 +517,8 @@ public class ProviderServiceTests
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
var expectedPlanType = PlanType.EnterpriseAnnually;
organization.PlanType = PlanType.EnterpriseAnnually;
var expectedPlanType = PlanType.EnterpriseMonthly;
organization.PlanType = PlanType.EnterpriseMonthly;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
@ -579,12 +547,12 @@ public class ProviderServiceTests
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<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
@ -663,11 +631,11 @@ public class ProviderServiceTests
public async Task CreateOrganizationAsync_Success(Provider provider, OrganizationSignup organizationSignup,
Organization organization, string clientOwnerEmail, User user, SutProvider<ProviderService> sutProvider)
{
organizationSignup.Plan = PlanType.EnterpriseAnnually;
organizationSignup.Plan = PlanType.EnterpriseMonthly;
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup)
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, new Collection()));
var providerOrganization =
@ -688,7 +656,7 @@ public class ProviderServiceTests
}
[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 class ProviderServiceTests
User user,
SutProvider<ProviderService> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
provider.Type = ProviderType.Msp;
provider.Status = ProviderStatusType.Billable;
@ -717,7 +683,7 @@ public class ProviderServiceTests
}
[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 class ProviderServiceTests
User user,
SutProvider<ProviderService> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
provider.Type = ProviderType.Msp;
provider.Status = ProviderStatusType.Billable;
@ -771,11 +735,11 @@ public class ProviderServiceTests
(Provider provider, OrganizationSignup organizationSignup, Organization organization, string clientOwnerEmail,
User user, SutProvider<ProviderService> sutProvider, Collection defaultCollection)
{
organizationSignup.Plan = PlanType.EnterpriseAnnually;
organizationSignup.Plan = PlanType.EnterpriseMonthly;
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup)
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, defaultCollection));
var providerOrganization =

View File

@ -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 class OrganizationsController : Controller
IServiceAccountRepository serviceAccountRepository,
IProviderOrganizationRepository providerOrganizationRepository,
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
IFeatureService featureService,
IProviderBillingService providerBillingService)
IProviderBillingService providerBillingService,
IFeatureService featureService)
{
_organizationService = organizationService;
_organizationRepository = organizationRepository;
@ -108,8 +108,8 @@ public class OrganizationsController : Controller
_serviceAccountRepository = serviceAccountRepository;
_providerOrganizationRepository = providerOrganizationRepository;
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
_featureService = featureService;
_providerBillingService = providerBillingService;
_featureService = featureService;
}
[RequirePermission(Permission.Org_List_View)]
@ -285,9 +285,7 @@ public class OrganizationsController : Controller
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 @@ public class OrganizationsController : Controller
Organization organization,
OrganizationEditModel update)
{
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
var scaleMSPOnClientOrganizationUpdate =
_featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate);
if (!consolidatedBillingEnabled || !scaleMSPOnClientOrganizationUpdate)
if (!scaleMSPOnClientOrganizationUpdate)
{
return;
}

View File

@ -282,9 +282,7 @@ public class ProvidersController : Controller
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 @@ public class ProvidersController : Controller
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<ProviderPlan>());
}

View File

@ -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 @@
<label asp-for="OwnerEmail"></label>
<input type="text" class="form-control" asp-for="OwnerEmail">
</div>
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
{
<div class="row">
<div class="col-sm">
<div class="form-group">
@ -33,7 +26,6 @@
</div>
</div>
</div>
}
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
</form>
</div>

View File

@ -48,7 +48,7 @@
</div>
</div>
</div>
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable())
@if (Model.Provider.IsBillable())
{
switch (Model.Provider.Type)
{
@ -68,46 +68,6 @@
</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<div class="form-group">
<div class="form-group">
<label asp-for="Gateway"></label>
<select class="form-control" asp-for="Gateway" asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
<option value="">--</option>
</select>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="GatewayCustomerId"></label>
<div class="input-group">
<input type="text" class="form-control" asp-for="GatewayCustomerId">
<div class="input-group-append">
<a href="@Model.GatewayCustomerUrl" class="btn btn-secondary" target="_blank">
<i class="fa fa-external-link"></i>
</a>
</div>
</div>
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="GatewaySubscriptionId"></label>
<div class="input-group">
<input type="text" class="form-control" asp-for="GatewaySubscriptionId">
<div class="input-group-append">
<a href="@Model.GatewaySubscriptionUrl" class="btn btn-secondary" target="_blank">
<i class="fa fa-external-link"></i>
</a>
</div>
</div>
</div>
</div>
</div>
break;
}
case ProviderType.MultiOrganizationEnterprise:
@ -141,6 +101,46 @@
break;
}
}
<div class="row">
<div class="col-sm">
<div class="form-group">
<div class="form-group">
<label asp-for="Gateway"></label>
<select class="form-control" asp-for="Gateway" asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
<option value="">--</option>
</select>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="GatewayCustomerId"></label>
<div class="input-group">
<input type="text" class="form-control" asp-for="GatewayCustomerId">
<div class="input-group-append">
<a href="@Model.GatewayCustomerUrl" class="btn btn-secondary" target="_blank">
<i class="fa fa-external-link"></i>
</a>
</div>
</div>
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="GatewaySubscriptionId"></label>
<div class="input-group">
<input type="text" class="form-control" asp-for="GatewaySubscriptionId">
<div class="input-group-append">
<a href="@Model.GatewaySubscriptionUrl" class="btn btn-secondary" target="_blank">
<i class="fa fa-external-link"></i>
</a>
</div>
</div>
</div>
</div>
</div>
}
</form>
@await Html.PartialAsync("Organizations", Model)

View File

@ -289,9 +289,7 @@ public class OrganizationsController : Controller
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 class OrganizationsController : Controller
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())

View File

@ -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<BaseProviderController> logger,
IProviderRepository providerRepository,
IUserService userService) : BaseBillingController
@ -26,15 +24,6 @@ public abstract class BaseProviderController(
Guid providerId,
Func<Guid, bool> 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)

View File

@ -19,14 +19,13 @@ namespace Bit.Api.Billing.Controllers;
[Authorize("Application")]
public class ProviderBillingController(
ICurrentContext currentContext,
IFeatureService featureService,
ILogger<BaseProviderController> 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<IResult> GetInvoicesAsync([FromRoute] Guid providerId)

View File

@ -14,14 +14,13 @@ namespace Bit.Api.Billing.Controllers;
[Route("providers/{providerId:guid}/clients")]
public class ProviderClientsController(
ICurrentContext currentContext,
IFeatureService featureService,
ILogger<BaseProviderController> 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<IResult> CreateAsync(

View File

@ -15,6 +15,7 @@ using Bit.Core.Auth.Models.Business.Tokenables;
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 class OrganizationService : IOrganizationService
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 class OrganizationService : IOrganizationService
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.");
}

View File

@ -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 class BillingExtensions
{
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

View File

@ -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";

View File

@ -0,0 +1,20 @@
using Bit.Core.Entities;
using Bit.Core.Utilities;
namespace Bit.Core.Vault.Entities;
public class SecurityTask : ITableObject<Guid>
{
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();
}
}

View File

@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Vault.Enums;
public enum SecurityTaskStatus : byte
{
/// <summary>
/// Default status for newly created tasks that have not been completed.
/// </summary>
[Display(Name = "Pending")]
Pending = 0,
/// <summary>
/// Status when a task is considered complete and has no remaining actions
/// </summary>
[Display(Name = "Completed")]
Completed = 1,
}

View File

@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Vault.Enums;
public enum SecurityTaskType : byte
{
/// <summary>
/// Task to update a cipher's password that was found to be at-risk by an administrator
/// </summary>
[Display(Name = "Update at-risk credential")]
UpdateAtRiskCredential = 0
}

View File

@ -0,0 +1,9 @@
using Bit.Core.Repositories;
using Bit.Core.Vault.Entities;
namespace Bit.Core.Vault.Repositories;
public interface ISecurityTaskRepository : IRepository<SecurityTask, Guid>
{
}

View File

@ -59,6 +59,7 @@ public static class DapperServiceCollectionExtensions
services
.AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
if (selfHosted)
{

View File

@ -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<SecurityTask, Guid>, ISecurityTaskRepository
{
public SecurityTaskRepository(GlobalSettings globalSettings)
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
{ }
public SecurityTaskRepository(string connectionString, string readOnlyConnectionString)
: base(connectionString, readOnlyConnectionString)
{ }
}

View File

@ -96,6 +96,7 @@ public static class EntityFrameworkServiceCollectionExtensions
services
.AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
if (selfHosted)
{

View File

@ -77,6 +77,7 @@ public class DatabaseContext : DbContext
public DbSet<NotificationStatus> NotificationStatuses { get; set; }
public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; }
public DbSet<PasswordHealthReportApplication> PasswordHealthReportApplications { get; set; }
public DbSet<SecurityTask> SecurityTasks { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{

View File

@ -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<SecurityTask>
{
public void Configure(EntityTypeBuilder<SecurityTask> 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));
}
}

View File

@ -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<Core.Vault.Entities.SecurityTask, SecurityTask>().ReverseMap();
}
}

View File

@ -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<Core.Vault.Entities.SecurityTask, SecurityTask, Guid>, ISecurityTaskRepository
{
public SecurityTaskRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
: base(serviceScopeFactory, mapper, (context) => context.SecurityTasks)
{ }
}

View File

@ -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

View File

@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[SecurityTask_ReadById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[SecurityTaskView]
WHERE
[Id] = @Id
END

View File

@ -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

View File

@ -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;

View File

@ -0,0 +1,6 @@
CREATE VIEW [dbo].[SecurityTaskView]
AS
SELECT
*
FROM
[dbo].[SecurityTask]

View File

@ -68,7 +68,6 @@ public class OrganizationsControllerTests
var featureService = sutProvider.GetDependency<IFeatureService>();
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 class OrganizationsControllerTests
var featureService = sutProvider.GetDependency<IFeatureService>();
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 class OrganizationsControllerTests
var featureService = sutProvider.GetDependency<IFeatureService>();
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 class OrganizationsControllerTests
var featureService = sutProvider.GetDependency<IFeatureService>();
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 class OrganizationsControllerTests
var featureService = sutProvider.GetDependency<IFeatureService>();
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 class OrganizationsControllerTests
var featureService = sutProvider.GetDependency<IFeatureService>();
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 class OrganizationsControllerTests
var featureService = sutProvider.GetDependency<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true);
var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };

View File

@ -218,8 +218,6 @@ public class OrganizationsControllerTests : IDisposable
_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);

View File

@ -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<ProviderBillingController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().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<ProviderBillingController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId).ReturnsNull();
var result = await sutProvider.Sut.GetInvoicesAsync(providerId);
@ -68,9 +51,6 @@ public class ProviderBillingControllerTests
Provider provider,
SutProvider<ProviderBillingController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id)
@ -86,9 +66,6 @@ public class ProviderBillingControllerTests
Provider provider,
SutProvider<ProviderBillingController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
provider.Type = ProviderType.Reseller;
provider.Status = ProviderStatusType.Created;
@ -229,27 +206,11 @@ public class ProviderBillingControllerTests
#region GetSubscriptionAsync & TryGetBillableProviderForServiceUserOperation
[Theory, BitAutoData]
public async Task GetSubscriptionAsync_FFDisabled_NotFound(
Guid providerId,
SutProvider<ProviderBillingController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().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<ProviderBillingController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId).ReturnsNull();
var result = await sutProvider.Sut.GetSubscriptionAsync(providerId);
@ -262,9 +223,6 @@ public class ProviderBillingControllerTests
Provider provider,
SutProvider<ProviderBillingController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
sutProvider.GetDependency<ICurrentContext>().ProviderUser(provider.Id)
@ -280,9 +238,6 @@ public class ProviderBillingControllerTests
Provider provider,
SutProvider<ProviderBillingController> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
provider.Type = ProviderType.Reseller;
provider.Status = ProviderStatusType.Created;

View File

@ -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 @@ public static class Utilities
Provider provider,
SutProvider<T> sutProvider) where T : BaseProviderController
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
.Returns(true);
provider.Type = ProviderType.Msp;
provider.Status = ProviderStatusType.Billable;

View File

@ -420,8 +420,6 @@ public class OrganizationServiceTests
OrganizationSignup signup,
SutProvider<OrganizationService> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
signup.Plan = PlanType.TeamsMonthly;
var (organization, _, _) = await sutProvider.Sut.SignupClientAsync(signup);

View File

@ -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);
}
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,59 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.MySqlMigrations.Migrations;
/// <inheritdoc />
public partial class SecurityTasks : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SecurityTask",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
OrganizationId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
CipherId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
Type = table.Column<byte>(type: "tinyint unsigned", nullable: false),
Status = table.Column<byte>(type: "tinyint unsigned", nullable: false),
CreationDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
RevisionDate = table.Column<DateTime>(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");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SecurityTask");
}
}

View File

@ -1989,6 +1989,41 @@ namespace Bit.MySqlMigrations.Migrations
b.ToTable("Folder", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b =>
{
b.Property<Guid>("Id")
.HasColumnType("char(36)");
b.Property<Guid?>("CipherId")
.HasColumnType("char(36)");
b.Property<DateTime>("CreationDate")
.HasColumnType("datetime(6)");
b.Property<Guid>("OrganizationId")
.HasColumnType("char(36)");
b.Property<DateTime>("RevisionDate")
.HasColumnType("datetime(6)");
b.Property<byte>("Status")
.HasColumnType("tinyint unsigned");
b.Property<byte>("Type")
.HasColumnType("tinyint unsigned");
b.HasKey("Id")
.HasAnnotation("SqlServer:Clustered", true);
b.HasIndex("CipherId")
.HasAnnotation("SqlServer:Clustered", false);
b.HasIndex("OrganizationId")
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("SecurityTask", (string)null);
});
modelBuilder.Entity("ProjectSecret", b =>
{
b.Property<Guid>("ProjectsId")
@ -2643,6 +2678,23 @@ namespace Bit.MySqlMigrations.Migrations
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)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.PostgresMigrations.Migrations;
/// <inheritdoc />
public partial class SecurityTasks : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SecurityTask",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
OrganizationId = table.Column<Guid>(type: "uuid", nullable: false),
CipherId = table.Column<Guid>(type: "uuid", nullable: true),
Type = table.Column<byte>(type: "smallint", nullable: false),
Status = table.Column<byte>(type: "smallint", nullable: false),
CreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
RevisionDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_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");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SecurityTask");
}
}

View File

@ -1995,6 +1995,41 @@ namespace Bit.PostgresMigrations.Migrations
b.ToTable("Folder", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<Guid?>("CipherId")
.HasColumnType("uuid");
b.Property<DateTime>("CreationDate")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("OrganizationId")
.HasColumnType("uuid");
b.Property<DateTime>("RevisionDate")
.HasColumnType("timestamp with time zone");
b.Property<byte>("Status")
.HasColumnType("smallint");
b.Property<byte>("Type")
.HasColumnType("smallint");
b.HasKey("Id")
.HasAnnotation("SqlServer:Clustered", true);
b.HasIndex("CipherId")
.HasAnnotation("SqlServer:Clustered", false);
b.HasIndex("OrganizationId")
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("SecurityTask", (string)null);
});
modelBuilder.Entity("ProjectSecret", b =>
{
b.Property<Guid>("ProjectsId")
@ -2649,6 +2684,23 @@ namespace Bit.PostgresMigrations.Migrations
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)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.SqliteMigrations.Migrations;
/// <inheritdoc />
public partial class SecurityTasks : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SecurityTask",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
OrganizationId = table.Column<Guid>(type: "TEXT", nullable: false),
CipherId = table.Column<Guid>(type: "TEXT", nullable: true),
Type = table.Column<byte>(type: "INTEGER", nullable: false),
Status = table.Column<byte>(type: "INTEGER", nullable: false),
CreationDate = table.Column<DateTime>(type: "TEXT", nullable: false),
RevisionDate = table.Column<DateTime>(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");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SecurityTask");
}
}

View File

@ -1978,6 +1978,41 @@ namespace Bit.SqliteMigrations.Migrations
b.ToTable("Folder", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT");
b.Property<Guid?>("CipherId")
.HasColumnType("TEXT");
b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT");
b.Property<Guid>("OrganizationId")
.HasColumnType("TEXT");
b.Property<DateTime>("RevisionDate")
.HasColumnType("TEXT");
b.Property<byte>("Status")
.HasColumnType("INTEGER");
b.Property<byte>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id")
.HasAnnotation("SqlServer:Clustered", true);
b.HasIndex("CipherId")
.HasAnnotation("SqlServer:Clustered", false);
b.HasIndex("OrganizationId")
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("SecurityTask", (string)null);
});
modelBuilder.Entity("ProjectSecret", b =>
{
b.Property<Guid>("ProjectsId")
@ -2632,6 +2667,23 @@ namespace Bit.SqliteMigrations.Migrations
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)