mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
Merge branch 'main' into PM-11162-assign-to-collection-perm-update
This commit is contained in:
commit
710b0e2695
@ -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.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
@ -10,7 +9,6 @@ using Bit.Core.Billing.Repositories;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.AdminConsole.Providers;
|
namespace Bit.Commercial.Core.AdminConsole.Providers;
|
||||||
|
|
||||||
@ -21,35 +19,28 @@ public class CreateProviderCommand : ICreateProviderCommand
|
|||||||
private readonly IProviderService _providerService;
|
private readonly IProviderService _providerService;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly IProviderPlanRepository _providerPlanRepository;
|
private readonly IProviderPlanRepository _providerPlanRepository;
|
||||||
private readonly IFeatureService _featureService;
|
|
||||||
|
|
||||||
public CreateProviderCommand(
|
public CreateProviderCommand(
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
IProviderUserRepository providerUserRepository,
|
IProviderUserRepository providerUserRepository,
|
||||||
IProviderService providerService,
|
IProviderService providerService,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IProviderPlanRepository providerPlanRepository,
|
IProviderPlanRepository providerPlanRepository)
|
||||||
IFeatureService featureService)
|
|
||||||
{
|
{
|
||||||
_providerRepository = providerRepository;
|
_providerRepository = providerRepository;
|
||||||
_providerUserRepository = providerUserRepository;
|
_providerUserRepository = providerUserRepository;
|
||||||
_providerService = providerService;
|
_providerService = providerService;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_providerPlanRepository = providerPlanRepository;
|
_providerPlanRepository = providerPlanRepository;
|
||||||
_featureService = featureService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats)
|
public async Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats)
|
||||||
{
|
{
|
||||||
var providerId = await CreateProviderAsync(provider, ownerEmail);
|
var providerId = await CreateProviderAsync(provider, ownerEmail);
|
||||||
|
|
||||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
await Task.WhenAll(
|
||||||
|
CreateProviderPlanAsync(providerId, PlanType.TeamsMonthly, teamsMinimumSeats),
|
||||||
if (isConsolidatedBillingEnabled)
|
CreateProviderPlanAsync(providerId, PlanType.EnterpriseMonthly, enterpriseMinimumSeats));
|
||||||
{
|
|
||||||
await CreateProviderPlanAsync(providerId, PlanType.TeamsMonthly, teamsMinimumSeats);
|
|
||||||
await CreateProviderPlanAsync(providerId, PlanType.EnterpriseMonthly, enterpriseMinimumSeats);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateResellerAsync(Provider provider)
|
public async Task CreateResellerAsync(Provider provider)
|
||||||
@ -61,13 +52,8 @@ public class CreateProviderCommand : ICreateProviderCommand
|
|||||||
{
|
{
|
||||||
var providerId = await CreateProviderAsync(provider, ownerEmail);
|
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)
|
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.");
|
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);
|
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Pending);
|
||||||
|
|
||||||
|
@ -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.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
@ -102,11 +100,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
IEnumerable<string> organizationOwnerEmails)
|
IEnumerable<string> organizationOwnerEmails)
|
||||||
{
|
{
|
||||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
if (provider.IsBillable() &&
|
||||||
|
organization.IsValidClient() &&
|
||||||
if (isConsolidatedBillingEnabled &&
|
|
||||||
provider.Status == ProviderStatusType.Billable &&
|
|
||||||
organization.Status == OrganizationStatusType.Managed &&
|
|
||||||
!string.IsNullOrEmpty(organization.GatewayCustomerId))
|
!string.IsNullOrEmpty(organization.GatewayCustomerId))
|
||||||
{
|
{
|
||||||
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
||||||
|
@ -8,7 +8,6 @@ using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
@ -101,13 +100,6 @@ public class ProviderService : IProviderService
|
|||||||
throw new BadRequestException("Invalid owner.");
|
throw new BadRequestException("Invalid owner.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
|
||||||
{
|
|
||||||
provider.Status = ProviderStatusType.Created;
|
|
||||||
await _providerRepository.UpsertAsync(provider);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (taxInfo == null || string.IsNullOrEmpty(taxInfo.BillingAddressCountry) || string.IsNullOrEmpty(taxInfo.BillingAddressPostalCode))
|
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.");
|
throw new BadRequestException("Both address and postal code are required to set up your provider.");
|
||||||
@ -118,7 +110,6 @@ public class ProviderService : IProviderService
|
|||||||
provider.GatewaySubscriptionId = subscription.Id;
|
provider.GatewaySubscriptionId = subscription.Id;
|
||||||
provider.Status = ProviderStatusType.Billable;
|
provider.Status = ProviderStatusType.Billable;
|
||||||
await _providerRepository.UpsertAsync(provider);
|
await _providerRepository.UpsertAsync(provider);
|
||||||
}
|
|
||||||
|
|
||||||
providerUser.Key = key;
|
providerUser.Key = key;
|
||||||
await _providerUserRepository.ReplaceAsync(providerUser);
|
await _providerUserRepository.ReplaceAsync(providerUser);
|
||||||
@ -545,13 +536,9 @@ public class ProviderService : IProviderService
|
|||||||
{
|
{
|
||||||
var provider = await _providerRepository.GetByIdAsync(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) = await _organizationService.SignupClientAsync(organizationSignup);
|
||||||
|
|
||||||
var (organization, _, defaultCollection) = consolidatedBillingEnabled
|
|
||||||
? await _organizationService.SignupClientAsync(organizationSignup)
|
|
||||||
: await _organizationService.SignUpAsync(organizationSignup);
|
|
||||||
|
|
||||||
var providerOrganization = new ProviderOrganization
|
var providerOrganization = new ProviderOrganization
|
||||||
{
|
{
|
||||||
@ -687,9 +674,7 @@ public class ProviderService : IProviderService
|
|||||||
return confirmedOwnersIds.Except(providerUserIds).Any();
|
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)
|
||||||
{
|
{
|
||||||
@ -708,7 +693,6 @@ public class ProviderService : IProviderService
|
|||||||
default:
|
default:
|
||||||
throw new BadRequestException($"Unsupported provider type {providerType}.");
|
throw new BadRequestException($"Unsupported provider type {providerType}.");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (ProviderDisallowedOrganizationTypes.Contains(requestedType))
|
if (ProviderDisallowedOrganizationTypes.Contains(requestedType))
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
@ -155,9 +154,6 @@ public class RemoveOrganizationFromProviderCommandTests
|
|||||||
"b@example.com"
|
"b@example.com"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
||||||
.Returns(false);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
|
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
|
||||||
.Returns(GetSubscription(organization.GatewaySubscriptionId));
|
.Returns(GetSubscription(organization.GatewaySubscriptionId));
|
||||||
|
|
||||||
@ -222,9 +218,6 @@ public class RemoveOrganizationFromProviderCommandTests
|
|||||||
"b@example.com"
|
"b@example.com"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using Bit.Commercial.Core.AdminConsole.Services;
|
using Bit.Commercial.Core.AdminConsole.Services;
|
||||||
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
|
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
@ -55,8 +54,8 @@ public class ProviderServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key,
|
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TaxInfo taxInfo,
|
||||||
[ProviderUser(ProviderUserStatusType.Confirmed, ProviderUserType.ProviderAdmin)] ProviderUser providerUser,
|
[ProviderUser] ProviderUser providerUser,
|
||||||
SutProvider<ProviderService> sutProvider)
|
SutProvider<ProviderService> sutProvider)
|
||||||
{
|
{
|
||||||
providerUser.ProviderId = provider.Id;
|
providerUser.ProviderId = provider.Id;
|
||||||
@ -71,37 +70,6 @@ public class ProviderServiceTests
|
|||||||
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
||||||
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
|
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
|
||||||
.Returns(protector);
|
.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,
|
|
||||||
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.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
|
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
|
||||||
|
|
||||||
@ -489,7 +457,7 @@ public class ProviderServiceTests
|
|||||||
public async Task AddOrganization_OrganizationHasSecretsManager_Throws(Provider provider, Organization organization, string key,
|
public async Task AddOrganization_OrganizationHasSecretsManager_Throws(Provider provider, Organization organization, string key,
|
||||||
SutProvider<ProviderService> sutProvider)
|
SutProvider<ProviderService> sutProvider)
|
||||||
{
|
{
|
||||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
organization.PlanType = PlanType.EnterpriseMonthly;
|
||||||
organization.UseSecretsManager = true;
|
organization.UseSecretsManager = true;
|
||||||
|
|
||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
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,
|
public async Task AddOrganization_Success(Provider provider, Organization organization, string key,
|
||||||
SutProvider<ProviderService> sutProvider)
|
SutProvider<ProviderService> sutProvider)
|
||||||
{
|
{
|
||||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
organization.PlanType = PlanType.EnterpriseMonthly;
|
||||||
|
|
||||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
@ -549,8 +517,8 @@ public class ProviderServiceTests
|
|||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
var expectedPlanType = PlanType.EnterpriseAnnually;
|
var expectedPlanType = PlanType.EnterpriseMonthly;
|
||||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
organization.PlanType = PlanType.EnterpriseMonthly;
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
|
||||||
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
||||||
@ -579,12 +547,12 @@ public class ProviderServiceTests
|
|||||||
BackdateProviderCreationDate(provider, newCreationDate);
|
BackdateProviderCreationDate(provider, newCreationDate);
|
||||||
provider.Type = ProviderType.Msp;
|
provider.Type = ProviderType.Msp;
|
||||||
|
|
||||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
organization.PlanType = PlanType.EnterpriseMonthly;
|
||||||
organization.Plan = "Enterprise (Annually)";
|
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);
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
@ -663,11 +631,11 @@ public class ProviderServiceTests
|
|||||||
public async Task CreateOrganizationAsync_Success(Provider provider, OrganizationSignup organizationSignup,
|
public async Task CreateOrganizationAsync_Success(Provider provider, OrganizationSignup organizationSignup,
|
||||||
Organization organization, string clientOwnerEmail, User user, SutProvider<ProviderService> sutProvider)
|
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);
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup)
|
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
||||||
.Returns((organization, null as OrganizationUser, new Collection()));
|
.Returns((organization, null as OrganizationUser, new Collection()));
|
||||||
|
|
||||||
var providerOrganization =
|
var providerOrganization =
|
||||||
@ -688,7 +656,7 @@ public class ProviderServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, OrganizationCustomize, BitAutoData]
|
[Theory, OrganizationCustomize, BitAutoData]
|
||||||
public async Task CreateOrganizationAsync_ConsolidatedBillingEnabled_InvalidPlanType_ThrowsBadRequestException(
|
public async Task CreateOrganizationAsync_InvalidPlanType_ThrowsBadRequestException(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
OrganizationSignup organizationSignup,
|
OrganizationSignup organizationSignup,
|
||||||
Organization organization,
|
Organization organization,
|
||||||
@ -696,8 +664,6 @@ public class ProviderServiceTests
|
|||||||
User user,
|
User user,
|
||||||
SutProvider<ProviderService> sutProvider)
|
SutProvider<ProviderService> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
|
||||||
|
|
||||||
provider.Type = ProviderType.Msp;
|
provider.Type = ProviderType.Msp;
|
||||||
provider.Status = ProviderStatusType.Billable;
|
provider.Status = ProviderStatusType.Billable;
|
||||||
|
|
||||||
@ -717,7 +683,7 @@ public class ProviderServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, OrganizationCustomize, BitAutoData]
|
[Theory, OrganizationCustomize, BitAutoData]
|
||||||
public async Task CreateOrganizationAsync_ConsolidatedBillingEnabled_InvokeSignupClientAsync(
|
public async Task CreateOrganizationAsync_InvokeSignupClientAsync(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
OrganizationSignup organizationSignup,
|
OrganizationSignup organizationSignup,
|
||||||
Organization organization,
|
Organization organization,
|
||||||
@ -725,8 +691,6 @@ public class ProviderServiceTests
|
|||||||
User user,
|
User user,
|
||||||
SutProvider<ProviderService> sutProvider)
|
SutProvider<ProviderService> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
|
||||||
|
|
||||||
provider.Type = ProviderType.Msp;
|
provider.Type = ProviderType.Msp;
|
||||||
provider.Status = ProviderStatusType.Billable;
|
provider.Status = ProviderStatusType.Billable;
|
||||||
|
|
||||||
@ -771,11 +735,11 @@ public class ProviderServiceTests
|
|||||||
(Provider provider, OrganizationSignup organizationSignup, Organization organization, string clientOwnerEmail,
|
(Provider provider, OrganizationSignup organizationSignup, Organization organization, string clientOwnerEmail,
|
||||||
User user, SutProvider<ProviderService> sutProvider, Collection defaultCollection)
|
User user, SutProvider<ProviderService> sutProvider, Collection defaultCollection)
|
||||||
{
|
{
|
||||||
organizationSignup.Plan = PlanType.EnterpriseAnnually;
|
organizationSignup.Plan = PlanType.EnterpriseMonthly;
|
||||||
|
|
||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup)
|
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
||||||
.Returns((organization, null as OrganizationUser, defaultCollection));
|
.Returns((organization, null as OrganizationUser, defaultCollection));
|
||||||
|
|
||||||
var providerOrganization =
|
var providerOrganization =
|
||||||
|
@ -55,8 +55,8 @@ public class OrganizationsController : Controller
|
|||||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||||
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
|
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
|
||||||
private readonly IFeatureService _featureService;
|
|
||||||
private readonly IProviderBillingService _providerBillingService;
|
private readonly IProviderBillingService _providerBillingService;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
public OrganizationsController(
|
public OrganizationsController(
|
||||||
IOrganizationService organizationService,
|
IOrganizationService organizationService,
|
||||||
@ -82,8 +82,8 @@ public class OrganizationsController : Controller
|
|||||||
IServiceAccountRepository serviceAccountRepository,
|
IServiceAccountRepository serviceAccountRepository,
|
||||||
IProviderOrganizationRepository providerOrganizationRepository,
|
IProviderOrganizationRepository providerOrganizationRepository,
|
||||||
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
||||||
IFeatureService featureService,
|
IProviderBillingService providerBillingService,
|
||||||
IProviderBillingService providerBillingService)
|
IFeatureService featureService)
|
||||||
{
|
{
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
@ -108,8 +108,8 @@ public class OrganizationsController : Controller
|
|||||||
_serviceAccountRepository = serviceAccountRepository;
|
_serviceAccountRepository = serviceAccountRepository;
|
||||||
_providerOrganizationRepository = providerOrganizationRepository;
|
_providerOrganizationRepository = providerOrganizationRepository;
|
||||||
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
|
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
|
||||||
_featureService = featureService;
|
|
||||||
_providerBillingService = providerBillingService;
|
_providerBillingService = providerBillingService;
|
||||||
|
_featureService = featureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequirePermission(Permission.Org_List_View)]
|
[RequirePermission(Permission.Org_List_View)]
|
||||||
@ -285,9 +285,7 @@ public class OrganizationsController : Controller
|
|||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
||||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
if (organization.IsValidClient())
|
||||||
|
|
||||||
if (consolidatedBillingEnabled && organization.IsValidClient())
|
|
||||||
{
|
{
|
||||||
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||||
|
|
||||||
@ -477,12 +475,10 @@ public class OrganizationsController : Controller
|
|||||||
Organization organization,
|
Organization organization,
|
||||||
OrganizationEditModel update)
|
OrganizationEditModel update)
|
||||||
{
|
{
|
||||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
|
||||||
|
|
||||||
var scaleMSPOnClientOrganizationUpdate =
|
var scaleMSPOnClientOrganizationUpdate =
|
||||||
_featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate);
|
_featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate);
|
||||||
|
|
||||||
if (!consolidatedBillingEnabled || !scaleMSPOnClientOrganizationUpdate)
|
if (!scaleMSPOnClientOrganizationUpdate)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -282,9 +282,7 @@ public class ProvidersController : Controller
|
|||||||
await _providerRepository.ReplaceAsync(provider);
|
await _providerRepository.ReplaceAsync(provider);
|
||||||
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
|
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
|
||||||
|
|
||||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
if (!provider.IsBillable())
|
||||||
|
|
||||||
if (!isConsolidatedBillingEnabled || !provider.IsBillable())
|
|
||||||
{
|
{
|
||||||
return RedirectToAction("Edit", new { id });
|
return RedirectToAction("Edit", new { id });
|
||||||
}
|
}
|
||||||
@ -340,10 +338,7 @@ public class ProvidersController : Controller
|
|||||||
var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);
|
var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);
|
||||||
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
|
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
|
||||||
|
|
||||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
if (!provider.IsBillable())
|
||||||
|
|
||||||
|
|
||||||
if (!isConsolidatedBillingEnabled || !provider.IsBillable())
|
|
||||||
{
|
{
|
||||||
return new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>());
|
return new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>());
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
@using Bit.Core.AdminConsole.Enums.Provider
|
|
||||||
@using Bit.Core
|
|
||||||
|
|
||||||
@model CreateMspProviderModel
|
@model CreateMspProviderModel
|
||||||
|
|
||||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Create Managed Service Provider";
|
ViewData["Title"] = "Create Managed Service Provider";
|
||||||
}
|
}
|
||||||
@ -17,8 +12,6 @@
|
|||||||
<label asp-for="OwnerEmail"></label>
|
<label asp-for="OwnerEmail"></label>
|
||||||
<input type="text" class="form-control" asp-for="OwnerEmail">
|
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||||
</div>
|
</div>
|
||||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
|
||||||
{
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -33,7 +26,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable())
|
@if (Model.Provider.IsBillable())
|
||||||
{
|
{
|
||||||
switch (Model.Provider.Type)
|
switch (Model.Provider.Type)
|
||||||
{
|
{
|
||||||
@ -68,6 +68,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ProviderType.MultiOrganizationEnterprise:
|
||||||
|
{
|
||||||
|
@if (FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) && Model.Provider.Type == ProviderType.MultiOrganizationEnterprise)
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
@{
|
||||||
|
var multiOrgPlans = new List<PlanType>
|
||||||
|
{
|
||||||
|
PlanType.EnterpriseAnnually,
|
||||||
|
PlanType.EnterpriseMonthly
|
||||||
|
};
|
||||||
|
}
|
||||||
|
<label asp-for="Plan"></label>
|
||||||
|
<select class="form-control" asp-for="Plan" asp-items="Html.GetEnumSelectList(multiOrgPlans)">
|
||||||
|
<option value="">--</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="EnterpriseMinimumSeats"></label>
|
||||||
|
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -108,39 +141,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ProviderType.MultiOrganizationEnterprise:
|
|
||||||
{
|
|
||||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) && Model.Provider.Type == ProviderType.MultiOrganizationEnterprise)
|
|
||||||
{
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm">
|
|
||||||
<div class="form-group">
|
|
||||||
@{
|
|
||||||
var multiOrgPlans = new List<PlanType>
|
|
||||||
{
|
|
||||||
PlanType.EnterpriseAnnually,
|
|
||||||
PlanType.EnterpriseMonthly
|
|
||||||
};
|
|
||||||
}
|
|
||||||
<label asp-for="Plan"></label>
|
|
||||||
<select class="form-control" asp-for="Plan" asp-items="Html.GetEnumSelectList(multiOrgPlans)">
|
|
||||||
<option value="">--</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm">
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="EnterpriseMinimumSeats"></label>
|
|
||||||
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
@await Html.PartialAsync("Organizations", Model)
|
@await Html.PartialAsync("Organizations", Model)
|
||||||
|
@ -289,9 +289,7 @@ public class OrganizationsController : Controller
|
|||||||
throw new BadRequestException(string.Empty, "User verification failed.");
|
throw new BadRequestException(string.Empty, "User verification failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
if (organization.IsValidClient())
|
||||||
|
|
||||||
if (consolidatedBillingEnabled && organization.IsValidClient())
|
|
||||||
{
|
{
|
||||||
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||||
|
|
||||||
@ -322,8 +320,7 @@ public class OrganizationsController : Controller
|
|||||||
throw new BadRequestException("Invalid token.");
|
throw new BadRequestException("Invalid token.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
if (organization.IsValidClient())
|
||||||
if (consolidatedBillingEnabled && organization.IsValidClient())
|
|
||||||
{
|
{
|
||||||
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||||
if (provider.IsBillable())
|
if (provider.IsBillable())
|
||||||
|
@ -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.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -9,7 +8,6 @@ namespace Bit.Api.Billing.Controllers;
|
|||||||
|
|
||||||
public abstract class BaseProviderController(
|
public abstract class BaseProviderController(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IFeatureService featureService,
|
|
||||||
ILogger<BaseProviderController> logger,
|
ILogger<BaseProviderController> logger,
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
IUserService userService) : BaseBillingController
|
IUserService userService) : BaseBillingController
|
||||||
@ -26,15 +24,6 @@ public abstract class BaseProviderController(
|
|||||||
Guid providerId,
|
Guid providerId,
|
||||||
Func<Guid, bool> checkAuthorization)
|
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);
|
var provider = await providerRepository.GetByIdAsync(providerId);
|
||||||
|
|
||||||
if (provider == null)
|
if (provider == null)
|
||||||
|
@ -19,14 +19,13 @@ namespace Bit.Api.Billing.Controllers;
|
|||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
public class ProviderBillingController(
|
public class ProviderBillingController(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IFeatureService featureService,
|
|
||||||
ILogger<BaseProviderController> logger,
|
ILogger<BaseProviderController> logger,
|
||||||
IProviderBillingService providerBillingService,
|
IProviderBillingService providerBillingService,
|
||||||
IProviderPlanRepository providerPlanRepository,
|
IProviderPlanRepository providerPlanRepository,
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
ISubscriberService subscriberService,
|
ISubscriberService subscriberService,
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
IUserService userService) : BaseProviderController(currentContext, featureService, logger, providerRepository, userService)
|
IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService)
|
||||||
{
|
{
|
||||||
[HttpGet("invoices")]
|
[HttpGet("invoices")]
|
||||||
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid providerId)
|
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid providerId)
|
||||||
|
@ -14,14 +14,13 @@ namespace Bit.Api.Billing.Controllers;
|
|||||||
[Route("providers/{providerId:guid}/clients")]
|
[Route("providers/{providerId:guid}/clients")]
|
||||||
public class ProviderClientsController(
|
public class ProviderClientsController(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IFeatureService featureService,
|
|
||||||
ILogger<BaseProviderController> logger,
|
ILogger<BaseProviderController> logger,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IProviderBillingService providerBillingService,
|
IProviderBillingService providerBillingService,
|
||||||
IProviderOrganizationRepository providerOrganizationRepository,
|
IProviderOrganizationRepository providerOrganizationRepository,
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
IProviderService providerService,
|
IProviderService providerService,
|
||||||
IUserService userService) : BaseProviderController(currentContext, featureService, logger, providerRepository, userService)
|
IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService)
|
||||||
{
|
{
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IResult> CreateAsync(
|
public async Task<IResult> CreateAsync(
|
||||||
|
@ -15,6 +15,7 @@ using Bit.Core.Auth.Models.Business.Tokenables;
|
|||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Models.Sales;
|
using Bit.Core.Billing.Models.Sales;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -444,13 +445,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup)
|
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);
|
var plan = StaticStore.GetPlan(signup.Plan);
|
||||||
|
|
||||||
ValidatePlan(plan, signup.AdditionalSeats, "Password Manager");
|
ValidatePlan(plan, signup.AdditionalSeats, "Password Manager");
|
||||||
@ -1443,10 +1437,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
if (provider is { Enabled: true })
|
if (provider is { Enabled: true })
|
||||||
{
|
{
|
||||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
if (provider.IsBillable())
|
||||||
|
|
||||||
if (consolidatedBillingEnabled && provider.Type == ProviderType.Msp &&
|
|
||||||
provider.Status == ProviderStatusType.Billable)
|
|
||||||
{
|
{
|
||||||
return (false, "Seat limit has been reached. Please contact your provider to add more seats.");
|
return (false, "Seat limit has been reached. Please contact your provider to add more seats.");
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,11 @@ namespace Bit.Core.Billing.Extensions;
|
|||||||
public static class BillingExtensions
|
public static class BillingExtensions
|
||||||
{
|
{
|
||||||
public static bool IsBillable(this Provider provider) =>
|
public static bool IsBillable(this Provider provider) =>
|
||||||
provider.SupportsConsolidatedBilling() && provider.Status == ProviderStatusType.Billable;
|
provider is
|
||||||
|
{
|
||||||
public static bool SupportsConsolidatedBilling(this Provider provider)
|
Type: ProviderType.Msp or ProviderType.MultiOrganizationEnterprise,
|
||||||
=> provider.Type.SupportsConsolidatedBilling();
|
Status: ProviderStatusType.Billable
|
||||||
|
};
|
||||||
|
|
||||||
public static bool SupportsConsolidatedBilling(this ProviderType providerType)
|
public static bool SupportsConsolidatedBilling(this ProviderType providerType)
|
||||||
=> providerType is ProviderType.Msp or ProviderType.MultiOrganizationEnterprise;
|
=> providerType is ProviderType.Msp or ProviderType.MultiOrganizationEnterprise;
|
||||||
@ -24,12 +25,15 @@ public static class BillingExtensions
|
|||||||
{
|
{
|
||||||
Seats: not null,
|
Seats: not null,
|
||||||
Status: OrganizationStatusType.Managed,
|
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)
|
public static bool IsStripeEnabled(this ISubscriber subscriber)
|
||||||
=> !string.IsNullOrEmpty(subscriber.GatewayCustomerId) &&
|
=> subscriber is
|
||||||
!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId);
|
{
|
||||||
|
GatewayCustomerId: not null and not "",
|
||||||
|
GatewaySubscriptionId: not null and not ""
|
||||||
|
};
|
||||||
|
|
||||||
public static bool IsUnverifiedBankAccount(this SetupIntent setupIntent) =>
|
public static bool IsUnverifiedBankAccount(this SetupIntent setupIntent) =>
|
||||||
setupIntent is
|
setupIntent is
|
||||||
|
@ -107,7 +107,6 @@ public static class FeatureFlagKeys
|
|||||||
public const string ItemShare = "item-share";
|
public const string ItemShare = "item-share";
|
||||||
public const string DuoRedirect = "duo-redirect";
|
public const string DuoRedirect = "duo-redirect";
|
||||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
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 AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
|
||||||
public const string EmailVerification = "email-verification";
|
public const string EmailVerification = "email-verification";
|
||||||
public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays";
|
public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays";
|
||||||
|
20
src/Core/Vault/Entities/SecurityTask.cs
Normal file
20
src/Core/Vault/Entities/SecurityTask.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
18
src/Core/Vault/Enums/SecurityTaskStatus.cs
Normal file
18
src/Core/Vault/Enums/SecurityTaskStatus.cs
Normal 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,
|
||||||
|
}
|
12
src/Core/Vault/Enums/SecurityTaskType.cs
Normal file
12
src/Core/Vault/Enums/SecurityTaskType.cs
Normal 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
|
||||||
|
}
|
9
src/Core/Vault/Repositories/ISecurityTaskRepository.cs
Normal file
9
src/Core/Vault/Repositories/ISecurityTaskRepository.cs
Normal 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>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
@ -59,6 +59,7 @@ public static class DapperServiceCollectionExtensions
|
|||||||
services
|
services
|
||||||
.AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();
|
.AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();
|
||||||
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
|
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
|
||||||
|
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
|
||||||
|
|
||||||
if (selfHosted)
|
if (selfHosted)
|
||||||
{
|
{
|
||||||
|
@ -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)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
}
|
@ -96,6 +96,7 @@ public static class EntityFrameworkServiceCollectionExtensions
|
|||||||
services
|
services
|
||||||
.AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();
|
.AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();
|
||||||
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
|
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
|
||||||
|
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
|
||||||
|
|
||||||
if (selfHosted)
|
if (selfHosted)
|
||||||
{
|
{
|
||||||
|
@ -77,6 +77,7 @@ public class DatabaseContext : DbContext
|
|||||||
public DbSet<NotificationStatus> NotificationStatuses { get; set; }
|
public DbSet<NotificationStatus> NotificationStatuses { get; set; }
|
||||||
public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; }
|
public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; }
|
||||||
public DbSet<PasswordHealthReportApplication> PasswordHealthReportApplications { get; set; }
|
public DbSet<PasswordHealthReportApplication> PasswordHealthReportApplications { get; set; }
|
||||||
|
public DbSet<SecurityTask> SecurityTasks { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
{ }
|
||||||
|
}
|
@ -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
|
@ -0,0 +1,13 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[SecurityTask_ReadById]
|
||||||
|
@Id UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[SecurityTaskView]
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
END
|
@ -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
|
21
src/Sql/Vault/dbo/Tables/SecurityTask.sql
Normal file
21
src/Sql/Vault/dbo/Tables/SecurityTask.sql
Normal 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;
|
6
src/Sql/Vault/dbo/Views/SecurityTaskView.sql
Normal file
6
src/Sql/Vault/dbo/Views/SecurityTaskView.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
CREATE VIEW [dbo].[SecurityTaskView]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[SecurityTask]
|
@ -68,7 +68,6 @@ public class OrganizationsControllerTests
|
|||||||
|
|
||||||
var featureService = sutProvider.GetDependency<IFeatureService>();
|
var featureService = sutProvider.GetDependency<IFeatureService>();
|
||||||
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true);
|
featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true);
|
||||||
|
|
||||||
var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Created };
|
var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Created };
|
||||||
@ -104,7 +103,6 @@ public class OrganizationsControllerTests
|
|||||||
|
|
||||||
var featureService = sutProvider.GetDependency<IFeatureService>();
|
var featureService = sutProvider.GetDependency<IFeatureService>();
|
||||||
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true);
|
featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true);
|
||||||
|
|
||||||
var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };
|
var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };
|
||||||
@ -147,7 +145,6 @@ public class OrganizationsControllerTests
|
|||||||
|
|
||||||
var featureService = sutProvider.GetDependency<IFeatureService>();
|
var featureService = sutProvider.GetDependency<IFeatureService>();
|
||||||
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true);
|
featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true);
|
||||||
|
|
||||||
var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };
|
var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };
|
||||||
@ -190,7 +187,6 @@ public class OrganizationsControllerTests
|
|||||||
|
|
||||||
var featureService = sutProvider.GetDependency<IFeatureService>();
|
var featureService = sutProvider.GetDependency<IFeatureService>();
|
||||||
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true);
|
featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true);
|
||||||
|
|
||||||
var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };
|
var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };
|
||||||
@ -233,7 +229,6 @@ public class OrganizationsControllerTests
|
|||||||
|
|
||||||
var featureService = sutProvider.GetDependency<IFeatureService>();
|
var featureService = sutProvider.GetDependency<IFeatureService>();
|
||||||
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true);
|
featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true);
|
||||||
|
|
||||||
var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };
|
var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };
|
||||||
@ -278,7 +273,6 @@ public class OrganizationsControllerTests
|
|||||||
|
|
||||||
var featureService = sutProvider.GetDependency<IFeatureService>();
|
var featureService = sutProvider.GetDependency<IFeatureService>();
|
||||||
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true);
|
featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true);
|
||||||
|
|
||||||
var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };
|
var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };
|
||||||
@ -322,7 +316,6 @@ public class OrganizationsControllerTests
|
|||||||
|
|
||||||
var featureService = sutProvider.GetDependency<IFeatureService>();
|
var featureService = sutProvider.GetDependency<IFeatureService>();
|
||||||
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true);
|
featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate).Returns(true);
|
||||||
|
|
||||||
var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };
|
var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };
|
||||||
|
@ -218,8 +218,6 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
|
|
||||||
_userService.VerifySecretAsync(user, requestModel.Secret).Returns(true);
|
_userService.VerifySecretAsync(user, requestModel.Secret).Returns(true);
|
||||||
|
|
||||||
_featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
|
||||||
|
|
||||||
_providerRepository.GetByOrganizationIdAsync(organization.Id).Returns(provider);
|
_providerRepository.GetByOrganizationIdAsync(organization.Id).Returns(provider);
|
||||||
|
|
||||||
await _sut.Delete(organizationId.ToString(), requestModel);
|
await _sut.Delete(organizationId.ToString(), requestModel);
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using Bit.Api.Billing.Controllers;
|
using Bit.Api.Billing.Controllers;
|
||||||
using Bit.Api.Billing.Models.Requests;
|
using Bit.Api.Billing.Models.Requests;
|
||||||
using Bit.Api.Billing.Models.Responses;
|
using Bit.Api.Billing.Models.Responses;
|
||||||
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.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
@ -35,27 +34,11 @@ public class ProviderBillingControllerTests
|
|||||||
{
|
{
|
||||||
#region GetInvoicesAsync & TryGetBillableProviderForAdminOperations
|
#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]
|
[Theory, BitAutoData]
|
||||||
public async Task GetInvoicesAsync_NullProvider_NotFound(
|
public async Task GetInvoicesAsync_NullProvider_NotFound(
|
||||||
Guid providerId,
|
Guid providerId,
|
||||||
SutProvider<ProviderBillingController> sutProvider)
|
SutProvider<ProviderBillingController> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId).ReturnsNull();
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId).ReturnsNull();
|
||||||
|
|
||||||
var result = await sutProvider.Sut.GetInvoicesAsync(providerId);
|
var result = await sutProvider.Sut.GetInvoicesAsync(providerId);
|
||||||
@ -68,9 +51,6 @@ public class ProviderBillingControllerTests
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingController> sutProvider)
|
SutProvider<ProviderBillingController> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id)
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id)
|
||||||
@ -86,9 +66,6 @@ public class ProviderBillingControllerTests
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingController> sutProvider)
|
SutProvider<ProviderBillingController> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
provider.Type = ProviderType.Reseller;
|
provider.Type = ProviderType.Reseller;
|
||||||
provider.Status = ProviderStatusType.Created;
|
provider.Status = ProviderStatusType.Created;
|
||||||
|
|
||||||
@ -229,27 +206,11 @@ public class ProviderBillingControllerTests
|
|||||||
|
|
||||||
#region GetSubscriptionAsync & TryGetBillableProviderForServiceUserOperation
|
#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]
|
[Theory, BitAutoData]
|
||||||
public async Task GetSubscriptionAsync_NullProvider_NotFound(
|
public async Task GetSubscriptionAsync_NullProvider_NotFound(
|
||||||
Guid providerId,
|
Guid providerId,
|
||||||
SutProvider<ProviderBillingController> sutProvider)
|
SutProvider<ProviderBillingController> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId).ReturnsNull();
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId).ReturnsNull();
|
||||||
|
|
||||||
var result = await sutProvider.Sut.GetSubscriptionAsync(providerId);
|
var result = await sutProvider.Sut.GetSubscriptionAsync(providerId);
|
||||||
@ -262,9 +223,6 @@ public class ProviderBillingControllerTests
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingController> sutProvider)
|
SutProvider<ProviderBillingController> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().ProviderUser(provider.Id)
|
sutProvider.GetDependency<ICurrentContext>().ProviderUser(provider.Id)
|
||||||
@ -280,9 +238,6 @@ public class ProviderBillingControllerTests
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingController> sutProvider)
|
SutProvider<ProviderBillingController> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
provider.Type = ProviderType.Reseller;
|
provider.Type = ProviderType.Reseller;
|
||||||
provider.Status = ProviderStatusType.Created;
|
provider.Status = ProviderStatusType.Created;
|
||||||
|
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
using Bit.Api.Billing.Controllers;
|
using Bit.Api.Billing.Controllers;
|
||||||
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.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
@ -59,9 +57,6 @@ public static class Utilities
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<T> sutProvider) where T : BaseProviderController
|
SutProvider<T> sutProvider) where T : BaseProviderController
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
provider.Type = ProviderType.Msp;
|
provider.Type = ProviderType.Msp;
|
||||||
provider.Status = ProviderStatusType.Billable;
|
provider.Status = ProviderStatusType.Billable;
|
||||||
|
|
||||||
|
@ -420,8 +420,6 @@ public class OrganizationServiceTests
|
|||||||
OrganizationSignup signup,
|
OrganizationSignup signup,
|
||||||
SutProvider<OrganizationService> sutProvider)
|
SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
|
||||||
|
|
||||||
signup.Plan = PlanType.TeamsMonthly;
|
signup.Plan = PlanType.TeamsMonthly;
|
||||||
|
|
||||||
var (organization, _, _) = await sutProvider.Sut.SignupClientAsync(signup);
|
var (organization, _, _) = await sutProvider.Sut.SignupClientAsync(signup);
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
114
util/Migrator/DbScripts/2024-11-11_00_InitialSecurityTasks.sql
Normal file
114
util/Migrator/DbScripts/2024-11-11_00_InitialSecurityTasks.sql
Normal 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
|
2940
util/MySqlMigrations/Migrations/20241112001902_SecurityTasks.Designer.cs
generated
Normal file
2940
util/MySqlMigrations/Migrations/20241112001902_SecurityTasks.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||||
|
}
|
||||||
|
}
|
@ -1989,6 +1989,41 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
b.ToTable("Folder", (string)null);
|
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 =>
|
modelBuilder.Entity("ProjectSecret", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("ProjectsId")
|
b.Property<Guid>("ProjectsId")
|
||||||
@ -2643,6 +2678,23 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
b.Navigation("User");
|
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 =>
|
modelBuilder.Entity("ProjectSecret", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null)
|
b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null)
|
||||||
|
2946
util/PostgresMigrations/Migrations/20241112001757_SecurityTasks.Designer.cs
generated
Normal file
2946
util/PostgresMigrations/Migrations/20241112001757_SecurityTasks.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||||
|
}
|
||||||
|
}
|
@ -1995,6 +1995,41 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
b.ToTable("Folder", (string)null);
|
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 =>
|
modelBuilder.Entity("ProjectSecret", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("ProjectsId")
|
b.Property<Guid>("ProjectsId")
|
||||||
@ -2649,6 +2684,23 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
b.Navigation("User");
|
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 =>
|
modelBuilder.Entity("ProjectSecret", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null)
|
b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null)
|
||||||
|
2929
util/SqliteMigrations/Migrations/20241112001814_SecurityTasks.Designer.cs
generated
Normal file
2929
util/SqliteMigrations/Migrations/20241112001814_SecurityTasks.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||||
|
}
|
||||||
|
}
|
@ -1978,6 +1978,41 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
b.ToTable("Folder", (string)null);
|
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 =>
|
modelBuilder.Entity("ProjectSecret", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("ProjectsId")
|
b.Property<Guid>("ProjectsId")
|
||||||
@ -2632,6 +2667,23 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
b.Navigation("User");
|
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 =>
|
modelBuilder.Entity("ProjectSecret", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null)
|
b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null)
|
||||||
|
Loading…
Reference in New Issue
Block a user