mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[PM-13447] Add Multi Org Enterprise providers to Admin Console (#4920)
This commit is contained in:
parent
39c560bbdd
commit
a952d10637
@ -40,6 +40,32 @@ public class CreateProviderCommand : ICreateProviderCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 providerPlans = new List<ProviderPlan>
|
||||||
|
{
|
||||||
|
CreateProviderPlan(provider.Id, PlanType.TeamsMonthly, teamsMinimumSeats),
|
||||||
|
CreateProviderPlan(provider.Id, PlanType.EnterpriseMonthly, enterpriseMinimumSeats)
|
||||||
|
};
|
||||||
|
|
||||||
|
await CreateProviderAsync(provider, ownerEmail, providerPlans);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateResellerAsync(Provider provider)
|
||||||
|
{
|
||||||
|
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Created);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateMultiOrganizationEnterpriseAsync(Provider provider, string ownerEmail, PlanType plan, int minimumSeats)
|
||||||
|
{
|
||||||
|
var providerPlans = new List<ProviderPlan>
|
||||||
|
{
|
||||||
|
CreateProviderPlan(provider.Id, plan, minimumSeats)
|
||||||
|
};
|
||||||
|
|
||||||
|
await CreateProviderAsync(provider, ownerEmail, providerPlans);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateProviderAsync(Provider provider, string ownerEmail, List<ProviderPlan> providerPlans)
|
||||||
{
|
{
|
||||||
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
|
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
|
||||||
if (owner == null)
|
if (owner == null)
|
||||||
@ -66,12 +92,6 @@ public class CreateProviderCommand : ICreateProviderCommand
|
|||||||
|
|
||||||
if (isConsolidatedBillingEnabled)
|
if (isConsolidatedBillingEnabled)
|
||||||
{
|
{
|
||||||
var providerPlans = new List<ProviderPlan>
|
|
||||||
{
|
|
||||||
CreateProviderPlan(provider.Id, PlanType.TeamsMonthly, teamsMinimumSeats),
|
|
||||||
CreateProviderPlan(provider.Id, PlanType.EnterpriseMonthly, enterpriseMinimumSeats)
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var providerPlan in providerPlans)
|
foreach (var providerPlan in providerPlans)
|
||||||
{
|
{
|
||||||
await _providerPlanRepository.CreateAsync(providerPlan);
|
await _providerPlanRepository.CreateAsync(providerPlan);
|
||||||
@ -82,11 +102,6 @@ public class CreateProviderCommand : ICreateProviderCommand
|
|||||||
await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateResellerAsync(Provider provider)
|
|
||||||
{
|
|
||||||
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Created);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ProviderRepositoryCreateAsync(Provider provider, ProviderStatusType status)
|
private async Task ProviderRepositoryCreateAsync(Provider provider, ProviderStatusType status)
|
||||||
{
|
{
|
||||||
provider.Status = status;
|
provider.Status = status;
|
||||||
|
@ -3,6 +3,7 @@ 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.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -19,23 +20,30 @@ public class CreateProviderCommandTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task CreateMspAsync_UserIdIsInvalid_Throws(Provider provider, SutProvider<CreateProviderCommand> sutProvider)
|
public async Task CreateMspAsync_UserIdIsInvalid_Throws(Provider provider, SutProvider<CreateProviderCommand> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
provider.Type = ProviderType.Msp;
|
provider.Type = ProviderType.Msp;
|
||||||
|
|
||||||
|
// Act
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.CreateMspAsync(provider, default, default, default));
|
() => sutProvider.Sut.CreateMspAsync(provider, default, default, default));
|
||||||
|
|
||||||
|
// Assert
|
||||||
Assert.Contains("Invalid owner.", exception.Message);
|
Assert.Contains("Invalid owner.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task CreateMspAsync_Success(Provider provider, User user, SutProvider<CreateProviderCommand> sutProvider)
|
public async Task CreateMspAsync_Success(Provider provider, User user, SutProvider<CreateProviderCommand> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
provider.Type = ProviderType.Msp;
|
provider.Type = ProviderType.Msp;
|
||||||
|
|
||||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
userRepository.GetByEmailAsync(user.Email).Returns(user);
|
userRepository.GetByEmailAsync(user.Email).Returns(user);
|
||||||
|
|
||||||
|
// Act
|
||||||
await sutProvider.Sut.CreateMspAsync(provider, user.Email, default, default);
|
await sutProvider.Sut.CreateMspAsync(provider, user.Email, default, default);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
||||||
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);
|
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);
|
||||||
}
|
}
|
||||||
@ -43,11 +51,52 @@ public class CreateProviderCommandTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task CreateResellerAsync_Success(Provider provider, SutProvider<CreateProviderCommand> sutProvider)
|
public async Task CreateResellerAsync_Success(Provider provider, SutProvider<CreateProviderCommand> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
provider.Type = ProviderType.Reseller;
|
provider.Type = ProviderType.Reseller;
|
||||||
|
|
||||||
|
// Act
|
||||||
await sutProvider.Sut.CreateResellerAsync(provider);
|
await sutProvider.Sut.CreateResellerAsync(provider);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
||||||
await sutProvider.GetDependency<IProviderService>().DidNotReceiveWithAnyArgs().SendProviderSetupInviteEmailAsync(default, default);
|
await sutProvider.GetDependency<IProviderService>().DidNotReceiveWithAnyArgs().SendProviderSetupInviteEmailAsync(default, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CreateMultiOrganizationEnterpriseAsync_Success(
|
||||||
|
Provider provider,
|
||||||
|
User user,
|
||||||
|
PlanType plan,
|
||||||
|
int minimumSeats,
|
||||||
|
SutProvider<CreateProviderCommand> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
provider.Type = ProviderType.MultiOrganizationEnterprise;
|
||||||
|
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
userRepository.GetByEmailAsync(user.Email).Returns(user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.CreateMultiOrganizationEnterpriseAsync(provider, user.Email, plan, minimumSeats);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(provider);
|
||||||
|
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CreateMultiOrganizationEnterpriseAsync_UserIdIsInvalid_Throws(
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<CreateProviderCommand> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
provider.Type = ProviderType.Msp;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.CreateMultiOrganizationEnterpriseAsync(provider, default, default, default));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("Invalid owner.", exception.Message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,9 +107,15 @@ public class ProvidersController : Controller
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public IActionResult Create(int teamsMinimumSeats, int enterpriseMinimumSeats, string ownerEmail = null)
|
public IActionResult Create()
|
||||||
{
|
{
|
||||||
return View(new CreateProviderModel
|
return View(new CreateProviderModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("providers/create/msp")]
|
||||||
|
public IActionResult CreateMsp(int teamsMinimumSeats, int enterpriseMinimumSeats, string ownerEmail = null)
|
||||||
|
{
|
||||||
|
return View(new CreateMspProviderModel
|
||||||
{
|
{
|
||||||
OwnerEmail = ownerEmail,
|
OwnerEmail = ownerEmail,
|
||||||
TeamsMonthlySeatMinimum = teamsMinimumSeats,
|
TeamsMonthlySeatMinimum = teamsMinimumSeats,
|
||||||
@ -117,10 +123,50 @@ public class ProvidersController : Controller
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("providers/create/reseller")]
|
||||||
|
public IActionResult CreateReseller()
|
||||||
|
{
|
||||||
|
return View(new CreateResellerProviderModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("providers/create/multi-organization-enterprise")]
|
||||||
|
public IActionResult CreateMultiOrganizationEnterprise(int enterpriseMinimumSeats, string ownerEmail = null)
|
||||||
|
{
|
||||||
|
if (!_featureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises))
|
||||||
|
{
|
||||||
|
return RedirectToAction("Create");
|
||||||
|
}
|
||||||
|
|
||||||
|
return View(new CreateMultiOrganizationEnterpriseProviderModel
|
||||||
|
{
|
||||||
|
OwnerEmail = ownerEmail,
|
||||||
|
EnterpriseSeatMinimum = enterpriseMinimumSeats
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
[RequirePermission(Permission.Provider_Create)]
|
[RequirePermission(Permission.Provider_Create)]
|
||||||
public async Task<IActionResult> Create(CreateProviderModel model)
|
public IActionResult Create(CreateProviderModel model)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.Type switch
|
||||||
|
{
|
||||||
|
ProviderType.Msp => RedirectToAction("CreateMsp"),
|
||||||
|
ProviderType.Reseller => RedirectToAction("CreateReseller"),
|
||||||
|
ProviderType.MultiOrganizationEnterprise => RedirectToAction("CreateMultiOrganizationEnterprise"),
|
||||||
|
_ => View(model)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("providers/create/msp")]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
[RequirePermission(Permission.Provider_Create)]
|
||||||
|
public async Task<IActionResult> CreateMsp(CreateMspProviderModel model)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
@ -128,19 +174,51 @@ public class ProvidersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var provider = model.ToProvider();
|
var provider = model.ToProvider();
|
||||||
switch (provider.Type)
|
|
||||||
|
await _createProviderCommand.CreateMspAsync(
|
||||||
|
provider,
|
||||||
|
model.OwnerEmail,
|
||||||
|
model.TeamsMonthlySeatMinimum,
|
||||||
|
model.EnterpriseMonthlySeatMinimum);
|
||||||
|
|
||||||
|
return RedirectToAction("Edit", new { id = provider.Id });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("providers/create/reseller")]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
[RequirePermission(Permission.Provider_Create)]
|
||||||
|
public async Task<IActionResult> CreateReseller(CreateResellerProviderModel model)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
case ProviderType.Msp:
|
return View(model);
|
||||||
await _createProviderCommand.CreateMspAsync(
|
|
||||||
provider,
|
|
||||||
model.OwnerEmail,
|
|
||||||
model.TeamsMonthlySeatMinimum,
|
|
||||||
model.EnterpriseMonthlySeatMinimum);
|
|
||||||
break;
|
|
||||||
case ProviderType.Reseller:
|
|
||||||
await _createProviderCommand.CreateResellerAsync(provider);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
var provider = model.ToProvider();
|
||||||
|
await _createProviderCommand.CreateResellerAsync(provider);
|
||||||
|
|
||||||
|
return RedirectToAction("Edit", new { id = provider.Id });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("providers/create/multi-organization-enterprise")]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
[RequirePermission(Permission.Provider_Create)]
|
||||||
|
public async Task<IActionResult> CreateMultiOrganizationEnterprise(CreateMultiOrganizationEnterpriseProviderModel model)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
var provider = model.ToProvider();
|
||||||
|
|
||||||
|
if (!_featureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises))
|
||||||
|
{
|
||||||
|
return RedirectToAction("Create");
|
||||||
|
}
|
||||||
|
await _createProviderCommand.CreateMultiOrganizationEnterpriseAsync(
|
||||||
|
provider,
|
||||||
|
model.OwnerEmail,
|
||||||
|
model.Plan.Value,
|
||||||
|
model.EnterpriseSeatMinimum);
|
||||||
|
|
||||||
return RedirectToAction("Edit", new { id = provider.Id });
|
return RedirectToAction("Edit", new { id = provider.Id });
|
||||||
}
|
}
|
||||||
|
45
src/Admin/AdminConsole/Models/CreateMspProviderModel.cs
Normal file
45
src/Admin/AdminConsole/Models/CreateMspProviderModel.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.SharedWeb.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Admin.AdminConsole.Models;
|
||||||
|
|
||||||
|
public class CreateMspProviderModel : IValidatableObject
|
||||||
|
{
|
||||||
|
[Display(Name = "Owner Email")]
|
||||||
|
public string OwnerEmail { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Teams (Monthly) Seat Minimum")]
|
||||||
|
public int TeamsMonthlySeatMinimum { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Enterprise (Monthly) Seat Minimum")]
|
||||||
|
public int EnterpriseMonthlySeatMinimum { get; set; }
|
||||||
|
|
||||||
|
public virtual Provider ToProvider()
|
||||||
|
{
|
||||||
|
return new Provider
|
||||||
|
{
|
||||||
|
Type = ProviderType.Msp
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(OwnerEmail))
|
||||||
|
{
|
||||||
|
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateMspProviderModel>()?.GetName() ?? nameof(OwnerEmail);
|
||||||
|
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
|
||||||
|
}
|
||||||
|
if (TeamsMonthlySeatMinimum < 0)
|
||||||
|
{
|
||||||
|
var teamsMinimumSeatsDisplayName = nameof(TeamsMonthlySeatMinimum).GetDisplayAttribute<CreateMspProviderModel>()?.GetName() ?? nameof(TeamsMonthlySeatMinimum);
|
||||||
|
yield return new ValidationResult($"The {teamsMinimumSeatsDisplayName} field can not be negative.");
|
||||||
|
}
|
||||||
|
if (EnterpriseMonthlySeatMinimum < 0)
|
||||||
|
{
|
||||||
|
var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMonthlySeatMinimum).GetDisplayAttribute<CreateMspProviderModel>()?.GetName() ?? nameof(EnterpriseMonthlySeatMinimum);
|
||||||
|
yield return new ValidationResult($"The {enterpriseMinimumSeatsDisplayName} field can not be negative.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.SharedWeb.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Admin.AdminConsole.Models;
|
||||||
|
|
||||||
|
public class CreateMultiOrganizationEnterpriseProviderModel : IValidatableObject
|
||||||
|
{
|
||||||
|
[Display(Name = "Owner Email")]
|
||||||
|
public string OwnerEmail { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Enterprise Seat Minimum")]
|
||||||
|
public int EnterpriseSeatMinimum { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Plan")]
|
||||||
|
[Required]
|
||||||
|
public PlanType? Plan { get; set; }
|
||||||
|
|
||||||
|
public virtual Provider ToProvider()
|
||||||
|
{
|
||||||
|
return new Provider
|
||||||
|
{
|
||||||
|
Type = ProviderType.MultiOrganizationEnterprise
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(OwnerEmail))
|
||||||
|
{
|
||||||
|
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateMultiOrganizationEnterpriseProviderModel>()?.GetName() ?? nameof(OwnerEmail);
|
||||||
|
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
|
||||||
|
}
|
||||||
|
if (EnterpriseSeatMinimum < 0)
|
||||||
|
{
|
||||||
|
var enterpriseSeatMinimumDisplayName = nameof(EnterpriseSeatMinimum).GetDisplayAttribute<CreateMultiOrganizationEnterpriseProviderModel>()?.GetName() ?? nameof(EnterpriseSeatMinimum);
|
||||||
|
yield return new ValidationResult($"The {enterpriseSeatMinimumDisplayName} field can not be negative.");
|
||||||
|
}
|
||||||
|
if (Plan != PlanType.EnterpriseAnnually && Plan != PlanType.EnterpriseMonthly)
|
||||||
|
{
|
||||||
|
var planDisplayName = nameof(Plan).GetDisplayAttribute<CreateMultiOrganizationEnterpriseProviderModel>()?.GetName() ?? nameof(Plan);
|
||||||
|
yield return new ValidationResult($"The {planDisplayName} field must be set to Enterprise Annually or Enterprise Monthly.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,84 +1,8 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
|
||||||
using Bit.SharedWeb.Utilities;
|
|
||||||
|
|
||||||
namespace Bit.Admin.AdminConsole.Models;
|
namespace Bit.Admin.AdminConsole.Models;
|
||||||
|
|
||||||
public class CreateProviderModel : IValidatableObject
|
public class CreateProviderModel
|
||||||
{
|
{
|
||||||
public CreateProviderModel() { }
|
|
||||||
|
|
||||||
[Display(Name = "Provider Type")]
|
|
||||||
public ProviderType Type { get; set; }
|
public ProviderType Type { get; set; }
|
||||||
|
|
||||||
[Display(Name = "Owner Email")]
|
|
||||||
public string OwnerEmail { get; set; }
|
|
||||||
|
|
||||||
[Display(Name = "Name")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
[Display(Name = "Business Name")]
|
|
||||||
public string BusinessName { get; set; }
|
|
||||||
|
|
||||||
[Display(Name = "Primary Billing Email")]
|
|
||||||
public string BillingEmail { get; set; }
|
|
||||||
|
|
||||||
[Display(Name = "Teams (Monthly) Seat Minimum")]
|
|
||||||
public int TeamsMonthlySeatMinimum { get; set; }
|
|
||||||
|
|
||||||
[Display(Name = "Enterprise (Monthly) Seat Minimum")]
|
|
||||||
public int EnterpriseMonthlySeatMinimum { get; set; }
|
|
||||||
|
|
||||||
public virtual Provider ToProvider()
|
|
||||||
{
|
|
||||||
return new Provider()
|
|
||||||
{
|
|
||||||
Type = Type,
|
|
||||||
Name = Name,
|
|
||||||
BusinessName = BusinessName,
|
|
||||||
BillingEmail = BillingEmail?.ToLowerInvariant().Trim()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
|
||||||
{
|
|
||||||
switch (Type)
|
|
||||||
{
|
|
||||||
case ProviderType.Msp:
|
|
||||||
if (string.IsNullOrWhiteSpace(OwnerEmail))
|
|
||||||
{
|
|
||||||
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(OwnerEmail);
|
|
||||||
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
|
|
||||||
}
|
|
||||||
if (TeamsMonthlySeatMinimum < 0)
|
|
||||||
{
|
|
||||||
var teamsMinimumSeatsDisplayName = nameof(TeamsMonthlySeatMinimum).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(TeamsMonthlySeatMinimum);
|
|
||||||
yield return new ValidationResult($"The {teamsMinimumSeatsDisplayName} field can not be negative.");
|
|
||||||
}
|
|
||||||
if (EnterpriseMonthlySeatMinimum < 0)
|
|
||||||
{
|
|
||||||
var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMonthlySeatMinimum).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMonthlySeatMinimum);
|
|
||||||
yield return new ValidationResult($"The {enterpriseMinimumSeatsDisplayName} field can not be negative.");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ProviderType.Reseller:
|
|
||||||
if (string.IsNullOrWhiteSpace(Name))
|
|
||||||
{
|
|
||||||
var nameDisplayName = nameof(Name).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(Name);
|
|
||||||
yield return new ValidationResult($"The {nameDisplayName} field is required.");
|
|
||||||
}
|
|
||||||
if (string.IsNullOrWhiteSpace(BusinessName))
|
|
||||||
{
|
|
||||||
var businessNameDisplayName = nameof(BusinessName).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BusinessName);
|
|
||||||
yield return new ValidationResult($"The {businessNameDisplayName} field is required.");
|
|
||||||
}
|
|
||||||
if (string.IsNullOrWhiteSpace(BillingEmail))
|
|
||||||
{
|
|
||||||
var billingEmailDisplayName = nameof(BillingEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BillingEmail);
|
|
||||||
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
48
src/Admin/AdminConsole/Models/CreateResellerProviderModel.cs
Normal file
48
src/Admin/AdminConsole/Models/CreateResellerProviderModel.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.SharedWeb.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Admin.AdminConsole.Models;
|
||||||
|
|
||||||
|
public class CreateResellerProviderModel : IValidatableObject
|
||||||
|
{
|
||||||
|
[Display(Name = "Name")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Business Name")]
|
||||||
|
public string BusinessName { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Primary Billing Email")]
|
||||||
|
public string BillingEmail { get; set; }
|
||||||
|
|
||||||
|
public virtual Provider ToProvider()
|
||||||
|
{
|
||||||
|
return new Provider
|
||||||
|
{
|
||||||
|
Name = Name,
|
||||||
|
BusinessName = BusinessName,
|
||||||
|
BillingEmail = BillingEmail?.ToLowerInvariant().Trim(),
|
||||||
|
Type = ProviderType.Reseller
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(Name))
|
||||||
|
{
|
||||||
|
var nameDisplayName = nameof(Name).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(Name);
|
||||||
|
yield return new ValidationResult($"The {nameDisplayName} field is required.");
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(BusinessName))
|
||||||
|
{
|
||||||
|
var businessNameDisplayName = nameof(BusinessName).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BusinessName);
|
||||||
|
yield return new ValidationResult($"The {businessNameDisplayName} field is required.");
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(BillingEmail))
|
||||||
|
{
|
||||||
|
var billingEmailDisplayName = nameof(BillingEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BillingEmail);
|
||||||
|
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,80 +1,48 @@
|
|||||||
@using Bit.SharedWeb.Utilities
|
@using Bit.SharedWeb.Utilities
|
||||||
@using Bit.Core.AdminConsole.Enums.Provider
|
@using Bit.Core.AdminConsole.Enums.Provider
|
||||||
@using Bit.Core
|
@using Bit.Core
|
||||||
|
|
||||||
@model CreateProviderModel
|
@model CreateProviderModel
|
||||||
|
|
||||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Create Provider";
|
ViewData["Title"] = "Create Provider";
|
||||||
}
|
|
||||||
|
|
||||||
@section Scripts {
|
var providerTypes = Enum.GetValues<ProviderType>()
|
||||||
<script>
|
.OrderBy(x => x.GetDisplayAttribute().Order)
|
||||||
function toggleProviderTypeInfo(value) {
|
.ToList();
|
||||||
document.querySelectorAll('[id^="info-"]').forEach(el => { el.classList.add('d-none'); });
|
|
||||||
document.getElementById('info-' + value).classList.remove('d-none');
|
if (!FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises))
|
||||||
}
|
{
|
||||||
</script>
|
providerTypes.Remove(ProviderType.MultiOrganizationEnterprise);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
<h1>Create Provider</h1>
|
<h1>Create Provider</h1>
|
||||||
|
<form method="post" asp-action="Create">
|
||||||
<form method="post">
|
|
||||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="Type" class="h2"></label>
|
<label asp-for="Type" class="h2"></label>
|
||||||
@foreach(ProviderType providerType in Enum.GetValues(typeof(ProviderType)))
|
@foreach (var providerType in providerTypes)
|
||||||
{
|
{
|
||||||
var providerTypeValue = (int)providerType;
|
var providerTypeValue = (int)providerType;
|
||||||
<div class="form-check">
|
<div class="form-group">
|
||||||
@Html.RadioButtonFor(m => m.Type, providerType, new { id = $"providerType-{providerTypeValue}", @class = "form-check-input", onclick=$"toggleProviderTypeInfo({providerTypeValue})" })
|
<div class="row">
|
||||||
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetName(), new { @class = "form-check-label align-middle", @for = $"providerType-{providerTypeValue}" })
|
<div class="col">
|
||||||
<br/>
|
<div class="form-check">
|
||||||
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetDescription(), new { @class = "form-check-label small text-muted ml-3 align-top", @for = $"providerType-{providerTypeValue}" })
|
@Html.RadioButtonFor(m => m.Type, providerType, new { id = $"providerType-{providerTypeValue}", @class = "form-check-input" })
|
||||||
</div>
|
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetName(), new { @class = "form-check-label align-middle", @for = $"providerType-{providerTypeValue}" })
|
||||||
}
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="@($"info-{(int)ProviderType.Msp}")" class="form-group @(Model.Type != ProviderType.Msp ? "d-none" : string.Empty)">
|
|
||||||
<h2>MSP Info</h2>
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="OwnerEmail"></label>
|
|
||||||
<input type="text" class="form-control" asp-for="OwnerEmail">
|
|
||||||
</div>
|
|
||||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
|
||||||
{
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm">
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
|
||||||
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
<div class="row">
|
||||||
<div class="form-group">
|
<div class="col">
|
||||||
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetDescription(), new { @class = "form-check-label small text-muted align-top", @for = $"providerType-{providerTypeValue}" })
|
||||||
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary mb-2">Next</button>
|
||||||
<div id="@($"info-{(int)ProviderType.Reseller}")" class="form-group @(Model.Type != ProviderType.Reseller ? "d-none" : string.Empty)">
|
|
||||||
<h2>Reseller Info</h2>
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="Name"></label>
|
|
||||||
<input type="text" class="form-control" asp-for="Name">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="BusinessName"></label>
|
|
||||||
<input type="text" class="form-control" asp-for="BusinessName">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="BillingEmail"></label>
|
|
||||||
<input type="text" class="form-control" asp-for="BillingEmail">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
39
src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml
Normal file
39
src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
@using Bit.Core.AdminConsole.Enums.Provider
|
||||||
|
@using Bit.Core
|
||||||
|
|
||||||
|
@model CreateMspProviderModel
|
||||||
|
|
||||||
|
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Create Managed Service Provider";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>Create Managed Service Provider</h1>
|
||||||
|
<div>
|
||||||
|
<form class="form-group" method="post" asp-action="CreateMsp">
|
||||||
|
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="OwnerEmail"></label>
|
||||||
|
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||||
|
</div>
|
||||||
|
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
||||||
|
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
||||||
|
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
@ -0,0 +1,43 @@
|
|||||||
|
@using Bit.Core.Billing.Enums
|
||||||
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
|
||||||
|
@model CreateMultiOrganizationEnterpriseProviderModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Create Multi-organization Enterprise Provider";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>Create Multi-organization Enterprise Provider</h1>
|
||||||
|
<div>
|
||||||
|
<form class="form-group" method="post" asp-action="CreateMultiOrganizationEnterprise">
|
||||||
|
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="OwnerEmail"></label>
|
||||||
|
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||||
|
</div>
|
||||||
|
<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="EnterpriseSeatMinimum"></label>
|
||||||
|
<input type="number" class="form-control" asp-for="EnterpriseSeatMinimum">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
25
src/Admin/AdminConsole/Views/Providers/CreateReseller.cshtml
Normal file
25
src/Admin/AdminConsole/Views/Providers/CreateReseller.cshtml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
@model CreateResellerProviderModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Create Reseller Provider";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>Create Reseller Provider</h1>
|
||||||
|
<div>
|
||||||
|
<form class="form-group" method="post" asp-action="CreateReseller">
|
||||||
|
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Name"></label>
|
||||||
|
<input type="text" class="form-control" asp-for="Name">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="BusinessName"></label>
|
||||||
|
<input type="text" class="form-control" asp-for="BusinessName">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="BillingEmail"></label>
|
||||||
|
<input type="text" class="form-control" asp-for="BillingEmail">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
19
src/Admin/Enums/HtmlHelperExtensions.cs
Normal file
19
src/Admin/Enums/HtmlHelperExtensions.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
using Bit.SharedWeb.Utilities;
|
||||||
|
|
||||||
|
// ReSharper disable once CheckNamespace
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
|
public static class HtmlHelper
|
||||||
|
{
|
||||||
|
public static IEnumerable<SelectListItem> GetEnumSelectList<T>(this IHtmlHelper htmlHelper, IEnumerable<T> values)
|
||||||
|
where T : Enum
|
||||||
|
{
|
||||||
|
return values.Select(v => new SelectListItem
|
||||||
|
{
|
||||||
|
Text = v.GetDisplayAttribute().Name,
|
||||||
|
Value = v.ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -4,8 +4,10 @@ namespace Bit.Core.AdminConsole.Enums.Provider;
|
|||||||
|
|
||||||
public enum ProviderType : byte
|
public enum ProviderType : byte
|
||||||
{
|
{
|
||||||
[Display(ShortName = "MSP", Name = "Managed Service Provider", Description = "Access to clients organization")]
|
[Display(ShortName = "MSP", Name = "Managed Service Provider", Description = "Access to clients organization", Order = 0)]
|
||||||
Msp = 0,
|
Msp = 0,
|
||||||
[Display(ShortName = "Reseller", Name = "Reseller", Description = "Access to clients billing")]
|
[Display(ShortName = "Reseller", Name = "Reseller", Description = "Access to clients billing", Order = 1000)]
|
||||||
Reseller = 1,
|
Reseller = 1,
|
||||||
|
[Display(ShortName = "MOE", Name = "Multi-organization Enterprise", Description = "Access to multiple organizations", Order = 1)]
|
||||||
|
MultiOrganizationEnterprise = 2,
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Providers.Interfaces;
|
namespace Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
|
|
||||||
@ -6,4 +7,5 @@ public interface ICreateProviderCommand
|
|||||||
{
|
{
|
||||||
Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats);
|
Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats);
|
||||||
Task CreateResellerAsync(Provider provider);
|
Task CreateResellerAsync(Provider provider);
|
||||||
|
Task CreateMultiOrganizationEnterpriseAsync(Provider provider, string ownerEmail, PlanType plan, int minimumSeats);
|
||||||
}
|
}
|
||||||
|
@ -146,6 +146,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string RemoveServerVersionHeader = "remove-server-version-header";
|
public const string RemoveServerVersionHeader = "remove-server-version-header";
|
||||||
public const string AccessIntelligence = "pm-13227-access-intelligence";
|
public const string AccessIntelligence = "pm-13227-access-intelligence";
|
||||||
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
||||||
|
public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises";
|
||||||
public const string Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions";
|
public const string Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions";
|
||||||
public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split";
|
public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split";
|
||||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
||||||
|
@ -0,0 +1,251 @@
|
|||||||
|
using Bit.Admin.AdminConsole.Controllers;
|
||||||
|
using Bit.Admin.AdminConsole.Models;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NSubstitute;
|
||||||
|
using NSubstitute.ReceivedExtensions;
|
||||||
|
|
||||||
|
namespace Admin.Test.AdminConsole.Controllers;
|
||||||
|
|
||||||
|
[ControllerCustomize(typeof(ProvidersController))]
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class ProvidersControllerTests
|
||||||
|
{
|
||||||
|
#region CreateMspAsync
|
||||||
|
[BitAutoData]
|
||||||
|
[SutProviderCustomize]
|
||||||
|
[Theory]
|
||||||
|
public async Task CreateMspAsync_WithValidModel_CreatesProvider(
|
||||||
|
CreateMspProviderModel model,
|
||||||
|
SutProvider<ProvidersController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = await sutProvider.Sut.CreateMsp(model);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(actual);
|
||||||
|
await sutProvider.GetDependency<ICreateProviderCommand>()
|
||||||
|
.Received(Quantity.Exactly(1))
|
||||||
|
.CreateMspAsync(
|
||||||
|
Arg.Is<Provider>(x => x.Type == ProviderType.Msp),
|
||||||
|
model.OwnerEmail,
|
||||||
|
model.TeamsMonthlySeatMinimum,
|
||||||
|
model.EnterpriseMonthlySeatMinimum);
|
||||||
|
}
|
||||||
|
|
||||||
|
[BitAutoData]
|
||||||
|
[SutProviderCustomize]
|
||||||
|
[Theory]
|
||||||
|
public async Task CreateMspAsync_RedirectsToExpectedPage_AfterCreatingProvider(
|
||||||
|
CreateMspProviderModel model,
|
||||||
|
Guid expectedProviderId,
|
||||||
|
SutProvider<ProvidersController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<ICreateProviderCommand>()
|
||||||
|
.When(x =>
|
||||||
|
x.CreateMspAsync(
|
||||||
|
Arg.Is<Provider>(y => y.Type == ProviderType.Msp),
|
||||||
|
model.OwnerEmail,
|
||||||
|
model.TeamsMonthlySeatMinimum,
|
||||||
|
model.EnterpriseMonthlySeatMinimum))
|
||||||
|
.Do(callInfo =>
|
||||||
|
{
|
||||||
|
var providerArgument = callInfo.ArgAt<Provider>(0);
|
||||||
|
providerArgument.Id = expectedProviderId;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = await sutProvider.Sut.CreateMsp(model);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(actual);
|
||||||
|
Assert.IsType<RedirectToActionResult>(actual);
|
||||||
|
var actualResult = (RedirectToActionResult)actual;
|
||||||
|
Assert.Equal("Edit", actualResult.ActionName);
|
||||||
|
Assert.Null(actualResult.ControllerName);
|
||||||
|
Assert.Equal(expectedProviderId, actualResult.RouteValues["Id"]);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region CreateMultiOrganizationEnterpriseAsync
|
||||||
|
[BitAutoData]
|
||||||
|
[SutProviderCustomize]
|
||||||
|
[Theory]
|
||||||
|
public async Task CreateMultiOrganizationEnterpriseAsync_WithValidModel_CreatesProvider(
|
||||||
|
CreateMultiOrganizationEnterpriseProviderModel model,
|
||||||
|
SutProvider<ProvidersController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = await sutProvider.Sut.CreateMultiOrganizationEnterprise(model);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(actual);
|
||||||
|
await sutProvider.GetDependency<ICreateProviderCommand>()
|
||||||
|
.Received(Quantity.Exactly(1))
|
||||||
|
.CreateMultiOrganizationEnterpriseAsync(
|
||||||
|
Arg.Is<Provider>(x => x.Type == ProviderType.MultiOrganizationEnterprise),
|
||||||
|
model.OwnerEmail,
|
||||||
|
Arg.Is<PlanType>(y => y == model.Plan),
|
||||||
|
model.EnterpriseSeatMinimum);
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.Received(Quantity.Exactly(1))
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises);
|
||||||
|
}
|
||||||
|
|
||||||
|
[BitAutoData]
|
||||||
|
[SutProviderCustomize]
|
||||||
|
[Theory]
|
||||||
|
public async Task CreateMultiOrganizationEnterpriseAsync_RedirectsToExpectedPage_AfterCreatingProvider(
|
||||||
|
CreateMultiOrganizationEnterpriseProviderModel model,
|
||||||
|
Guid expectedProviderId,
|
||||||
|
SutProvider<ProvidersController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<ICreateProviderCommand>()
|
||||||
|
.When(x =>
|
||||||
|
x.CreateMultiOrganizationEnterpriseAsync(
|
||||||
|
Arg.Is<Provider>(y => y.Type == ProviderType.MultiOrganizationEnterprise),
|
||||||
|
model.OwnerEmail,
|
||||||
|
Arg.Is<PlanType>(y => y == model.Plan),
|
||||||
|
model.EnterpriseSeatMinimum))
|
||||||
|
.Do(callInfo =>
|
||||||
|
{
|
||||||
|
var providerArgument = callInfo.ArgAt<Provider>(0);
|
||||||
|
providerArgument.Id = expectedProviderId;
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = await sutProvider.Sut.CreateMultiOrganizationEnterprise(model);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(actual);
|
||||||
|
Assert.IsType<RedirectToActionResult>(actual);
|
||||||
|
var actualResult = (RedirectToActionResult)actual;
|
||||||
|
Assert.Equal("Edit", actualResult.ActionName);
|
||||||
|
Assert.Null(actualResult.ControllerName);
|
||||||
|
Assert.Equal(expectedProviderId, actualResult.RouteValues["Id"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[BitAutoData]
|
||||||
|
[SutProviderCustomize]
|
||||||
|
[Theory]
|
||||||
|
public async Task CreateMultiOrganizationEnterpriseAsync_ChecksFeatureFlag(
|
||||||
|
CreateMultiOrganizationEnterpriseProviderModel model,
|
||||||
|
SutProvider<ProvidersController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.CreateMultiOrganizationEnterprise(model);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.Received(Quantity.Exactly(1))
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises);
|
||||||
|
}
|
||||||
|
|
||||||
|
[BitAutoData]
|
||||||
|
[SutProviderCustomize]
|
||||||
|
[Theory]
|
||||||
|
public async Task CreateMultiOrganizationEnterpriseAsync_RedirectsToProviderTypeSelectionPage_WhenFeatureFlagIsDisabled(
|
||||||
|
CreateMultiOrganizationEnterpriseProviderModel model,
|
||||||
|
SutProvider<ProvidersController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises)
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = await sutProvider.Sut.CreateMultiOrganizationEnterprise(model);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.Received(Quantity.Exactly(1))
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises);
|
||||||
|
|
||||||
|
Assert.IsType<RedirectToActionResult>(actual);
|
||||||
|
var actualResult = (RedirectToActionResult)actual;
|
||||||
|
Assert.Equal("Create", actualResult.ActionName);
|
||||||
|
Assert.Null(actualResult.ControllerName);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region CreateResellerAsync
|
||||||
|
[BitAutoData]
|
||||||
|
[SutProviderCustomize]
|
||||||
|
[Theory]
|
||||||
|
public async Task CreateResellerAsync_WithValidModel_CreatesProvider(
|
||||||
|
CreateResellerProviderModel model,
|
||||||
|
SutProvider<ProvidersController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = await sutProvider.Sut.CreateReseller(model);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(actual);
|
||||||
|
await sutProvider.GetDependency<ICreateProviderCommand>()
|
||||||
|
.Received(Quantity.Exactly(1))
|
||||||
|
.CreateResellerAsync(
|
||||||
|
Arg.Is<Provider>(x => x.Type == ProviderType.Reseller));
|
||||||
|
}
|
||||||
|
|
||||||
|
[BitAutoData]
|
||||||
|
[SutProviderCustomize]
|
||||||
|
[Theory]
|
||||||
|
public async Task CreateResellerAsync_RedirectsToExpectedPage_AfterCreatingProvider(
|
||||||
|
CreateResellerProviderModel model,
|
||||||
|
Guid expectedProviderId,
|
||||||
|
SutProvider<ProvidersController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<ICreateProviderCommand>()
|
||||||
|
.When(x =>
|
||||||
|
x.CreateResellerAsync(
|
||||||
|
Arg.Is<Provider>(y => y.Type == ProviderType.Reseller)))
|
||||||
|
.Do(callInfo =>
|
||||||
|
{
|
||||||
|
var providerArgument = callInfo.ArgAt<Provider>(0);
|
||||||
|
providerArgument.Id = expectedProviderId;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = await sutProvider.Sut.CreateReseller(model);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(actual);
|
||||||
|
Assert.IsType<RedirectToActionResult>(actual);
|
||||||
|
var actualResult = (RedirectToActionResult)actual;
|
||||||
|
Assert.Equal("Edit", actualResult.ActionName);
|
||||||
|
Assert.Null(actualResult.ControllerName);
|
||||||
|
Assert.Equal(expectedProviderId, actualResult.RouteValues["Id"]);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user