mirror of
https://github.com/bitwarden/server.git
synced 2025-04-27 07:42:15 -05:00
Merge branch 'main' into feature/phishing-detection
This commit is contained in:
commit
ce60dc8355
@ -1,9 +1,9 @@
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Pricing;
|
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
namespace Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||||
|
|
||||||
@ -11,16 +11,13 @@ public class MaxProjectsQuery : IMaxProjectsQuery
|
|||||||
{
|
{
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IProjectRepository _projectRepository;
|
private readonly IProjectRepository _projectRepository;
|
||||||
private readonly IPricingClient _pricingClient;
|
|
||||||
|
|
||||||
public MaxProjectsQuery(
|
public MaxProjectsQuery(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IProjectRepository projectRepository,
|
IProjectRepository projectRepository)
|
||||||
IPricingClient pricingClient)
|
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_projectRepository = projectRepository;
|
_projectRepository = projectRepository;
|
||||||
_pricingClient = pricingClient;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(short? max, bool? overMax)> GetByOrgIdAsync(Guid organizationId, int projectsToAdd)
|
public async Task<(short? max, bool? overMax)> GetByOrgIdAsync(Guid organizationId, int projectsToAdd)
|
||||||
@ -31,7 +28,8 @@ public class MaxProjectsQuery : IMaxProjectsQuery
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var plan = await _pricingClient.GetPlan(org.PlanType);
|
// TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-17122
|
||||||
|
var plan = StaticStore.GetPlan(org.PlanType);
|
||||||
if (plan?.SecretsManager == null)
|
if (plan?.SecretsManager == null)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Existing plan not found.");
|
throw new BadRequestException("Existing plan not found.");
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Pricing;
|
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
@ -68,9 +66,6 @@ public class MaxProjectsQueryTests
|
|||||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
||||||
{
|
{
|
||||||
organization.PlanType = planType;
|
organization.PlanType = planType;
|
||||||
|
|
||||||
sutProvider.GetDependency<IPricingClient>().GetPlan(planType).Returns(StaticStore.GetPlan(planType));
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
|
||||||
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
|
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
|
||||||
@ -111,9 +106,6 @@ public class MaxProjectsQueryTests
|
|||||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
||||||
{
|
{
|
||||||
organization.PlanType = planType;
|
organization.PlanType = planType;
|
||||||
|
|
||||||
sutProvider.GetDependency<IPricingClient>().GetPlan(planType).Returns(StaticStore.GetPlan(planType));
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
|
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
|
||||||
.Returns(projects);
|
.Returns(projects);
|
||||||
|
@ -462,6 +462,7 @@ public class OrganizationsController : Controller
|
|||||||
organization.UsersGetPremium = model.UsersGetPremium;
|
organization.UsersGetPremium = model.UsersGetPremium;
|
||||||
organization.UseSecretsManager = model.UseSecretsManager;
|
organization.UseSecretsManager = model.UseSecretsManager;
|
||||||
organization.UseRiskInsights = model.UseRiskInsights;
|
organization.UseRiskInsights = model.UseRiskInsights;
|
||||||
|
organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;
|
||||||
|
|
||||||
//secrets
|
//secrets
|
||||||
organization.SmSeats = model.SmSeats;
|
organization.SmSeats = model.SmSeats;
|
||||||
|
@ -3,11 +3,13 @@ using System.Net;
|
|||||||
using Bit.Admin.AdminConsole.Models;
|
using Bit.Admin.AdminConsole.Models;
|
||||||
using Bit.Admin.Enums;
|
using Bit.Admin.Enums;
|
||||||
using Bit.Admin.Utilities;
|
using Bit.Admin.Utilities;
|
||||||
|
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;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.Billing.Entities;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
@ -23,6 +25,7 @@ using Bit.Core.Settings;
|
|||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Admin.AdminConsole.Controllers;
|
namespace Bit.Admin.AdminConsole.Controllers;
|
||||||
|
|
||||||
@ -44,6 +47,7 @@ public class ProvidersController : Controller
|
|||||||
private readonly IProviderPlanRepository _providerPlanRepository;
|
private readonly IProviderPlanRepository _providerPlanRepository;
|
||||||
private readonly IProviderBillingService _providerBillingService;
|
private readonly IProviderBillingService _providerBillingService;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
|
private readonly IStripeAdapter _stripeAdapter;
|
||||||
private readonly string _stripeUrl;
|
private readonly string _stripeUrl;
|
||||||
private readonly string _braintreeMerchantUrl;
|
private readonly string _braintreeMerchantUrl;
|
||||||
private readonly string _braintreeMerchantId;
|
private readonly string _braintreeMerchantId;
|
||||||
@ -63,7 +67,8 @@ public class ProvidersController : Controller
|
|||||||
IProviderPlanRepository providerPlanRepository,
|
IProviderPlanRepository providerPlanRepository,
|
||||||
IProviderBillingService providerBillingService,
|
IProviderBillingService providerBillingService,
|
||||||
IWebHostEnvironment webHostEnvironment,
|
IWebHostEnvironment webHostEnvironment,
|
||||||
IPricingClient pricingClient)
|
IPricingClient pricingClient,
|
||||||
|
IStripeAdapter stripeAdapter)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
@ -79,6 +84,7 @@ public class ProvidersController : Controller
|
|||||||
_providerPlanRepository = providerPlanRepository;
|
_providerPlanRepository = providerPlanRepository;
|
||||||
_providerBillingService = providerBillingService;
|
_providerBillingService = providerBillingService;
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
|
_stripeAdapter = stripeAdapter;
|
||||||
_stripeUrl = webHostEnvironment.GetStripeUrl();
|
_stripeUrl = webHostEnvironment.GetStripeUrl();
|
||||||
_braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl();
|
_braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl();
|
||||||
_braintreeMerchantId = globalSettings.Braintree.MerchantId;
|
_braintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||||
@ -306,6 +312,23 @@ public class ProvidersController : Controller
|
|||||||
(Plan: PlanType.EnterpriseMonthly, SeatsMinimum: model.EnterpriseMonthlySeatMinimum)
|
(Plan: PlanType.EnterpriseMonthly, SeatsMinimum: model.EnterpriseMonthlySeatMinimum)
|
||||||
]);
|
]);
|
||||||
await _providerBillingService.UpdateSeatMinimums(updateMspSeatMinimumsCommand);
|
await _providerBillingService.UpdateSeatMinimums(updateMspSeatMinimumsCommand);
|
||||||
|
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically))
|
||||||
|
{
|
||||||
|
var customer = await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId);
|
||||||
|
|
||||||
|
if (model.PayByInvoice != customer.ApprovedToPayByInvoice())
|
||||||
|
{
|
||||||
|
var approvedToPayByInvoice = model.PayByInvoice ? "1" : "0";
|
||||||
|
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||||
|
{
|
||||||
|
Metadata = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
[StripeConstants.MetadataKeys.InvoiceApproved] = approvedToPayByInvoice
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case ProviderType.BusinessUnit:
|
case ProviderType.BusinessUnit:
|
||||||
{
|
{
|
||||||
@ -345,14 +368,18 @@ public class ProvidersController : Controller
|
|||||||
|
|
||||||
if (!provider.IsBillable())
|
if (!provider.IsBillable())
|
||||||
{
|
{
|
||||||
return new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>());
|
return new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||||
|
|
||||||
|
var payByInvoice =
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically) &&
|
||||||
|
(await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId)).ApprovedToPayByInvoice();
|
||||||
|
|
||||||
return new ProviderEditModel(
|
return new ProviderEditModel(
|
||||||
provider, users, providerOrganizations,
|
provider, users, providerOrganizations,
|
||||||
providerPlans.ToList(), GetGatewayCustomerUrl(provider), GetGatewaySubscriptionUrl(provider));
|
providerPlans.ToList(), payByInvoice, GetGatewayCustomerUrl(provider), GetGatewaySubscriptionUrl(provider));
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequirePermission(Permission.Provider_ResendEmailInvite)]
|
[RequirePermission(Permission.Provider_ResendEmailInvite)]
|
||||||
|
@ -18,6 +18,7 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
|||||||
IEnumerable<ProviderUserUserDetails> providerUsers,
|
IEnumerable<ProviderUserUserDetails> providerUsers,
|
||||||
IEnumerable<ProviderOrganizationOrganizationDetails> organizations,
|
IEnumerable<ProviderOrganizationOrganizationDetails> organizations,
|
||||||
IReadOnlyCollection<ProviderPlan> providerPlans,
|
IReadOnlyCollection<ProviderPlan> providerPlans,
|
||||||
|
bool payByInvoice,
|
||||||
string gatewayCustomerUrl = null,
|
string gatewayCustomerUrl = null,
|
||||||
string gatewaySubscriptionUrl = null) : base(provider, providerUsers, organizations, providerPlans)
|
string gatewaySubscriptionUrl = null) : base(provider, providerUsers, organizations, providerPlans)
|
||||||
{
|
{
|
||||||
@ -33,6 +34,7 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
|||||||
GatewayCustomerUrl = gatewayCustomerUrl;
|
GatewayCustomerUrl = gatewayCustomerUrl;
|
||||||
GatewaySubscriptionUrl = gatewaySubscriptionUrl;
|
GatewaySubscriptionUrl = gatewaySubscriptionUrl;
|
||||||
Type = provider.Type;
|
Type = provider.Type;
|
||||||
|
PayByInvoice = payByInvoice;
|
||||||
|
|
||||||
if (Type == ProviderType.BusinessUnit)
|
if (Type == ProviderType.BusinessUnit)
|
||||||
{
|
{
|
||||||
@ -62,6 +64,8 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
|||||||
public string GatewaySubscriptionId { get; set; }
|
public string GatewaySubscriptionId { get; set; }
|
||||||
public string GatewayCustomerUrl { get; }
|
public string GatewayCustomerUrl { get; }
|
||||||
public string GatewaySubscriptionUrl { get; }
|
public string GatewaySubscriptionUrl { get; }
|
||||||
|
[Display(Name = "Pay By Invoice")]
|
||||||
|
public bool PayByInvoice { get; set; }
|
||||||
[Display(Name = "Provider Type")]
|
[Display(Name = "Provider Type")]
|
||||||
public ProviderType Type { get; set; }
|
public ProviderType Type { get; set; }
|
||||||
|
|
||||||
|
@ -136,6 +136,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if (FeatureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically) && Model.Provider.Type == ProviderType.Msp && Model.Provider.IsBillable())
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input type="checkbox" class="form-check-input" asp-for="PayByInvoice">
|
||||||
|
<label class="form-check-label" asp-for="PayByInvoice"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
@await Html.PartialAsync("Organizations", Model)
|
@await Html.PartialAsync("Organizations", Model)
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
@using Bit.Admin.Enums;
|
@using Bit.Admin.Enums;
|
||||||
|
@using Bit.Core
|
||||||
@using Bit.Core.Enums
|
@using Bit.Core.Enums
|
||||||
@using Bit.Core.AdminConsole.Enums.Provider
|
@using Bit.Core.AdminConsole.Enums.Provider
|
||||||
@using Bit.Core.Billing.Enums
|
@using Bit.Core.Billing.Enums
|
||||||
@using Bit.SharedWeb.Utilities
|
@using Bit.SharedWeb.Utilities
|
||||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService;
|
@inject Bit.Admin.Services.IAccessControlService AccessControlService;
|
||||||
|
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||||
|
|
||||||
@model OrganizationEditModel
|
@model OrganizationEditModel
|
||||||
|
|
||||||
@ -146,6 +148,13 @@
|
|||||||
<input type="checkbox" class="form-check-input" asp-for="UseCustomPermissions" disabled='@(canEditPlan ? null : "disabled")'>
|
<input type="checkbox" class="form-check-input" asp-for="UseCustomPermissions" disabled='@(canEditPlan ? null : "disabled")'>
|
||||||
<label class="form-check-label" asp-for="UseCustomPermissions"></label>
|
<label class="form-check-label" asp-for="UseCustomPermissions"></label>
|
||||||
</div>
|
</div>
|
||||||
|
@if(FeatureService.IsEnabled(FeatureFlagKeys.PM17772_AdminInitiatedSponsorships))
|
||||||
|
{
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" asp-for="UseAdminSponsoredFamilies" disabled='@(canEditPlan ? null : "disabled")'>
|
||||||
|
<label class="form-check-label" asp-for="UseAdminSponsoredFamilies"></label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
<h3>Password Manager</h3>
|
<h3>Password Manager</h3>
|
||||||
|
@ -284,52 +284,6 @@ public class AccountsController : Controller
|
|||||||
throw new BadRequestException(ModelState);
|
throw new BadRequestException(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("set-key-connector-key")]
|
|
||||||
public async Task PostSetKeyConnectorKeyAsync([FromBody] SetKeyConnectorKeyRequestModel model)
|
|
||||||
{
|
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
throw new UnauthorizedAccessException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier);
|
|
||||||
if (result.Succeeded)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var error in result.Errors)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(string.Empty, error.Description);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new BadRequestException(ModelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("convert-to-key-connector")]
|
|
||||||
public async Task PostConvertToKeyConnector()
|
|
||||||
{
|
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
throw new UnauthorizedAccessException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _userService.ConvertToKeyConnectorAsync(user);
|
|
||||||
if (result.Succeeded)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var error in result.Errors)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(string.Empty, error.Description);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new BadRequestException(ModelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("kdf")]
|
[HttpPost("kdf")]
|
||||||
public async Task PostKdf([FromBody] KdfRequestModel model)
|
public async Task PostKdf([FromBody] KdfRequestModel model)
|
||||||
{
|
{
|
||||||
|
@ -24,7 +24,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
|
|
||||||
namespace Bit.Api.KeyManagement.Controllers;
|
namespace Bit.Api.KeyManagement.Controllers;
|
||||||
|
|
||||||
[Route("accounts/key-management")]
|
[Route("accounts")]
|
||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
public class AccountsKeyManagementController : Controller
|
public class AccountsKeyManagementController : Controller
|
||||||
{
|
{
|
||||||
@ -77,7 +77,7 @@ public class AccountsKeyManagementController : Controller
|
|||||||
_deviceValidator = deviceValidator;
|
_deviceValidator = deviceValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("regenerate-keys")]
|
[HttpPost("key-management/regenerate-keys")]
|
||||||
public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel request)
|
public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel request)
|
||||||
{
|
{
|
||||||
if (!_featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration))
|
if (!_featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration))
|
||||||
@ -93,7 +93,7 @@ public class AccountsKeyManagementController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpPost("rotate-user-account-keys")]
|
[HttpPost("key-management/rotate-user-account-keys")]
|
||||||
public async Task RotateUserAccountKeysAsync([FromBody] RotateUserAccountKeysAndDataRequestModel model)
|
public async Task RotateUserAccountKeysAsync([FromBody] RotateUserAccountKeysAndDataRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
@ -133,4 +133,50 @@ public class AccountsKeyManagementController : Controller
|
|||||||
|
|
||||||
throw new BadRequestException(ModelState);
|
throw new BadRequestException(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("set-key-connector-key")]
|
||||||
|
public async Task PostSetKeyConnectorKeyAsync([FromBody] SetKeyConnectorKeyRequestModel model)
|
||||||
|
{
|
||||||
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier);
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var error in result.Errors)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(string.Empty, error.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestException(ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("convert-to-key-connector")]
|
||||||
|
public async Task PostConvertToKeyConnectorAsync()
|
||||||
|
{
|
||||||
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _userService.ConvertToKeyConnectorAsync(user);
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var error in result.Errors)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(string.Empty, error.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestException(ModelState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ using Bit.Core.Auth.Models.Api.Request.Accounts;
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.Api.Auth.Models.Request.Accounts;
|
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||||
|
|
||||||
public class SetKeyConnectorKeyRequestModel
|
public class SetKeyConnectorKeyRequestModel
|
||||||
{
|
{
|
@ -177,12 +177,7 @@ public class CiphersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _cipherService.SaveDetailsAsync(cipher, user.Id, model.Cipher.LastKnownRevisionDate, model.CollectionIds, cipher.OrganizationId.HasValue);
|
await _cipherService.SaveDetailsAsync(cipher, user.Id, model.Cipher.LastKnownRevisionDate, model.CollectionIds, cipher.OrganizationId.HasValue);
|
||||||
var response = new CipherResponseModel(
|
return await Get(cipher.Id);
|
||||||
cipher,
|
|
||||||
user,
|
|
||||||
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
|
|
||||||
_globalSettings);
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("admin")]
|
[HttpPost("admin")]
|
||||||
|
@ -16,6 +16,12 @@ public interface IStripeFacade
|
|||||||
RequestOptions requestOptions = null,
|
RequestOptions requestOptions = null,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<Customer> UpdateCustomer(
|
||||||
|
string customerId,
|
||||||
|
CustomerUpdateOptions customerUpdateOptions = null,
|
||||||
|
RequestOptions requestOptions = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task<Event> GetEvent(
|
Task<Event> GetEvent(
|
||||||
string eventId,
|
string eventId,
|
||||||
EventGetOptions eventGetOptions = null,
|
EventGetOptions eventGetOptions = null,
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
using Bit.Billing.Constants;
|
using Bit.Billing.Constants;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
using Event = Stripe.Event;
|
using Event = Stripe.Event;
|
||||||
|
|
||||||
@ -10,20 +14,114 @@ public class PaymentMethodAttachedHandler : IPaymentMethodAttachedHandler
|
|||||||
private readonly IStripeEventService _stripeEventService;
|
private readonly IStripeEventService _stripeEventService;
|
||||||
private readonly IStripeFacade _stripeFacade;
|
private readonly IStripeFacade _stripeFacade;
|
||||||
private readonly IStripeEventUtilityService _stripeEventUtilityService;
|
private readonly IStripeEventUtilityService _stripeEventUtilityService;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
public PaymentMethodAttachedHandler(
|
public PaymentMethodAttachedHandler(
|
||||||
ILogger<PaymentMethodAttachedHandler> logger,
|
ILogger<PaymentMethodAttachedHandler> logger,
|
||||||
IStripeEventService stripeEventService,
|
IStripeEventService stripeEventService,
|
||||||
IStripeFacade stripeFacade,
|
IStripeFacade stripeFacade,
|
||||||
IStripeEventUtilityService stripeEventUtilityService)
|
IStripeEventUtilityService stripeEventUtilityService,
|
||||||
|
IFeatureService featureService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_stripeEventService = stripeEventService;
|
_stripeEventService = stripeEventService;
|
||||||
_stripeFacade = stripeFacade;
|
_stripeFacade = stripeFacade;
|
||||||
_stripeEventUtilityService = stripeEventUtilityService;
|
_stripeEventUtilityService = stripeEventUtilityService;
|
||||||
|
_featureService = featureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task HandleAsync(Event parsedEvent)
|
public async Task HandleAsync(Event parsedEvent)
|
||||||
|
{
|
||||||
|
var updateMSPToChargeAutomatically =
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically);
|
||||||
|
|
||||||
|
if (updateMSPToChargeAutomatically)
|
||||||
|
{
|
||||||
|
await HandleVNextAsync(parsedEvent);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await HandleVCurrentAsync(parsedEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleVNextAsync(Event parsedEvent)
|
||||||
|
{
|
||||||
|
var paymentMethod = await _stripeEventService.GetPaymentMethod(parsedEvent, true, ["customer.subscriptions.data.latest_invoice"]);
|
||||||
|
|
||||||
|
if (paymentMethod == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Attempted to handle the event payment_method.attached but paymentMethod was null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var customer = paymentMethod.Customer;
|
||||||
|
var subscriptions = customer?.Subscriptions;
|
||||||
|
|
||||||
|
// This represents a provider subscription set to "send_invoice" that was paid using a Stripe hosted invoice payment page.
|
||||||
|
var invoicedProviderSubscription = subscriptions?.Data.FirstOrDefault(subscription =>
|
||||||
|
subscription.Metadata.ContainsKey(StripeConstants.MetadataKeys.ProviderId) &&
|
||||||
|
subscription.Status != StripeConstants.SubscriptionStatus.Canceled &&
|
||||||
|
subscription.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If we have an invoiced provider subscription where the customer hasn't been marked as invoice-approved,
|
||||||
|
* we need to try and set the default payment method and update the collection method to be "charge_automatically".
|
||||||
|
*/
|
||||||
|
if (invoicedProviderSubscription != null && !customer.ApprovedToPayByInvoice())
|
||||||
|
{
|
||||||
|
if (customer.InvoiceSettings.DefaultPaymentMethodId != paymentMethod.Id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _stripeFacade.UpdateCustomer(customer.Id,
|
||||||
|
new CustomerUpdateOptions
|
||||||
|
{
|
||||||
|
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||||
|
{
|
||||||
|
DefaultPaymentMethod = paymentMethod.Id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(exception,
|
||||||
|
"Failed to set customer's ({CustomerID}) default payment method during 'payment_method.attached' webhook",
|
||||||
|
customer.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _stripeFacade.UpdateSubscription(invoicedProviderSubscription.Id,
|
||||||
|
new SubscriptionUpdateOptions
|
||||||
|
{
|
||||||
|
CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(exception,
|
||||||
|
"Failed to set subscription's ({SubscriptionID}) collection method to 'charge_automatically' during 'payment_method.attached' webhook",
|
||||||
|
customer.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var unpaidSubscriptions = subscriptions?.Data.Where(subscription =>
|
||||||
|
subscription.Status == StripeConstants.SubscriptionStatus.Unpaid).ToList();
|
||||||
|
|
||||||
|
if (unpaidSubscriptions == null || unpaidSubscriptions.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var unpaidSubscription in unpaidSubscriptions)
|
||||||
|
{
|
||||||
|
await AttemptToPayOpenSubscriptionAsync(unpaidSubscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleVCurrentAsync(Event parsedEvent)
|
||||||
{
|
{
|
||||||
var paymentMethod = await _stripeEventService.GetPaymentMethod(parsedEvent);
|
var paymentMethod = await _stripeEventService.GetPaymentMethod(parsedEvent);
|
||||||
if (paymentMethod is null)
|
if (paymentMethod is null)
|
||||||
|
@ -33,6 +33,13 @@ public class StripeFacade : IStripeFacade
|
|||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default) =>
|
||||||
await _customerService.GetAsync(customerId, customerGetOptions, requestOptions, cancellationToken);
|
await _customerService.GetAsync(customerId, customerGetOptions, requestOptions, cancellationToken);
|
||||||
|
|
||||||
|
public async Task<Customer> UpdateCustomer(
|
||||||
|
string customerId,
|
||||||
|
CustomerUpdateOptions customerUpdateOptions = null,
|
||||||
|
RequestOptions requestOptions = null,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
await _customerService.UpdateAsync(customerId, customerUpdateOptions, requestOptions, cancellationToken);
|
||||||
|
|
||||||
public async Task<Invoice> GetInvoice(
|
public async Task<Invoice> GetInvoice(
|
||||||
string invoiceId,
|
string invoiceId,
|
||||||
InvoiceGetOptions invoiceGetOptions = null,
|
InvoiceGetOptions invoiceGetOptions = null,
|
||||||
|
@ -206,10 +206,26 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
|||||||
|
|
||||||
private async Task SendAdditionalEmailsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
private async Task SendAdditionalEmailsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||||
{
|
{
|
||||||
await SendPasswordManagerMaxSeatLimitEmailsAsync(validatedResult, organization);
|
await NotifyOwnersIfAutoscaleOccursAsync(validatedResult, organization);
|
||||||
|
await NotifyOwnersIfPasswordManagerMaxSeatLimitReachedAsync(validatedResult, organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendPasswordManagerMaxSeatLimitEmailsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
private async Task NotifyOwnersIfAutoscaleOccursAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||||
|
{
|
||||||
|
if (validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd > 0
|
||||||
|
&& !organization.OwnersNotifiedOfAutoscaling.HasValue)
|
||||||
|
{
|
||||||
|
await mailService.SendOrganizationAutoscaledEmailAsync(
|
||||||
|
organization,
|
||||||
|
validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats!.Value,
|
||||||
|
await GetOwnerEmailAddressesAsync(validatedResult.Value.InviteOrganization));
|
||||||
|
|
||||||
|
organization.OwnersNotifiedOfAutoscaling = validatedResult.Value.PerformedAt.UtcDateTime;
|
||||||
|
await organizationRepository.UpsertAsync(organization);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task NotifyOwnersIfPasswordManagerMaxSeatLimitReachedAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||||
{
|
{
|
||||||
if (!validatedResult.Value.PasswordManagerSubscriptionUpdate.MaxSeatsReached)
|
if (!validatedResult.Value.PasswordManagerSubscriptionUpdate.MaxSeatsReached)
|
||||||
{
|
{
|
||||||
|
@ -8,6 +8,7 @@ public interface IRegisterUserCommand
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new user, sends a welcome email, and raises the signup reference event.
|
/// Creates a new user, sends a welcome email, and raises the signup reference event.
|
||||||
|
/// This method is used for JIT of organization Users.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user">The <see cref="User"/> to create</param>
|
/// <param name="user">The <see cref="User"/> to create</param>
|
||||||
/// <returns><see cref="IdentityResult"/></returns>
|
/// <returns><see cref="IdentityResult"/></returns>
|
||||||
|
@ -46,6 +46,7 @@ public static class StripeConstants
|
|||||||
|
|
||||||
public static class MetadataKeys
|
public static class MetadataKeys
|
||||||
{
|
{
|
||||||
|
public const string InvoiceApproved = "invoice_approved";
|
||||||
public const string OrganizationId = "organizationId";
|
public const string OrganizationId = "organizationId";
|
||||||
public const string ProviderId = "providerId";
|
public const string ProviderId = "providerId";
|
||||||
public const string UserId = "userId";
|
public const string UserId = "userId";
|
||||||
|
@ -27,4 +27,8 @@ public static class CustomerExtensions
|
|||||||
{
|
{
|
||||||
return customer != null ? customer.Balance / 100M : default;
|
return customer != null ? customer.Balance / 100M : default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool ApprovedToPayByInvoice(this Customer customer)
|
||||||
|
=> customer.Metadata.TryGetValue(StripeConstants.MetadataKeys.InvoiceApproved, out var value) &&
|
||||||
|
int.TryParse(value, out var invoiceApproved) && invoiceApproved == 1;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Models.Api.Requests.Organizations;
|
namespace Bit.Core.Billing.Models.Api.Requests.Organizations;
|
||||||
|
|
||||||
@ -20,6 +21,8 @@ public class OrganizationPasswordManagerRequestModel
|
|||||||
{
|
{
|
||||||
public PlanType Plan { get; set; }
|
public PlanType Plan { get; set; }
|
||||||
|
|
||||||
|
public PlanSponsorshipType? SponsoredPlan { get; set; }
|
||||||
|
|
||||||
[Range(0, int.MaxValue)]
|
[Range(0, int.MaxValue)]
|
||||||
public int Seats { get; set; }
|
public int Seats { get; set; }
|
||||||
|
|
||||||
|
@ -149,6 +149,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string PM19147_AutomaticTaxImprovements = "pm-19147-automatic-tax-improvements";
|
public const string PM19147_AutomaticTaxImprovements = "pm-19147-automatic-tax-improvements";
|
||||||
public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates";
|
public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates";
|
||||||
public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion";
|
public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion";
|
||||||
|
public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically";
|
||||||
|
|
||||||
/* Key Management Team */
|
/* Key Management Team */
|
||||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||||
@ -189,7 +190,6 @@ public static class FeatureFlagKeys
|
|||||||
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
|
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
|
||||||
public const string DesktopSendUIRefresh = "desktop-send-ui-refresh";
|
public const string DesktopSendUIRefresh = "desktop-send-ui-refresh";
|
||||||
public const string ExportAttachments = "export-attachments";
|
public const string ExportAttachments = "export-attachments";
|
||||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
|
||||||
|
|
||||||
/* Vault Team */
|
/* Vault Team */
|
||||||
public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge";
|
public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge";
|
||||||
@ -202,6 +202,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string CipherKeyEncryption = "cipher-key-encryption";
|
public const string CipherKeyEncryption = "cipher-key-encryption";
|
||||||
public const string DesktopCipherForms = "pm-18520-desktop-cipher-forms";
|
public const string DesktopCipherForms = "pm-18520-desktop-cipher-forms";
|
||||||
public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk";
|
public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk";
|
||||||
|
public const string EndUserNotifications = "pm-10609-end-user-notifications";
|
||||||
public const string PhishingDetection = "phishing-detection";
|
public const string PhishingDetection = "phishing-detection";
|
||||||
|
|
||||||
public static List<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
|
@ -1265,7 +1265,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options);
|
var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options);
|
||||||
|
|
||||||
var effectiveTaxRate = invoice.Tax != null && invoice.TotalExcludingTax != null
|
var effectiveTaxRate = invoice.Tax != null && invoice.TotalExcludingTax != null && invoice.TotalExcludingTax.Value != 0
|
||||||
? invoice.Tax.Value.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor()
|
? invoice.Tax.Value.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor()
|
||||||
: 0M;
|
: 0M;
|
||||||
|
|
||||||
@ -1300,6 +1300,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
string gatewaySubscriptionId)
|
string gatewaySubscriptionId)
|
||||||
{
|
{
|
||||||
var plan = await _pricingClient.GetPlanOrThrow(parameters.PasswordManager.Plan);
|
var plan = await _pricingClient.GetPlanOrThrow(parameters.PasswordManager.Plan);
|
||||||
|
var isSponsored = parameters.PasswordManager.SponsoredPlan.HasValue;
|
||||||
|
|
||||||
var options = new InvoiceCreatePreviewOptions
|
var options = new InvoiceCreatePreviewOptions
|
||||||
{
|
{
|
||||||
@ -1325,45 +1326,47 @@ public class StripePaymentService : IPaymentService
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (plan.PasswordManager.HasAdditionalSeatsOption)
|
if (isSponsored)
|
||||||
{
|
{
|
||||||
|
var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(parameters.PasswordManager.SponsoredPlan.Value);
|
||||||
options.SubscriptionDetails.Items.Add(
|
options.SubscriptionDetails.Items.Add(
|
||||||
new()
|
new() { Quantity = 1, Plan = sponsoredPlan.StripePlanId }
|
||||||
{
|
|
||||||
Quantity = parameters.PasswordManager.Seats,
|
|
||||||
Plan = plan.PasswordManager.StripeSeatPlanId
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
options.SubscriptionDetails.Items.Add(
|
if (plan.PasswordManager.HasAdditionalSeatsOption)
|
||||||
new()
|
|
||||||
{
|
|
||||||
Quantity = 1,
|
|
||||||
Plan = plan.PasswordManager.StripePlanId
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plan.SupportsSecretsManager)
|
|
||||||
{
|
|
||||||
if (plan.SecretsManager.HasAdditionalSeatsOption)
|
|
||||||
{
|
{
|
||||||
options.SubscriptionDetails.Items.Add(new()
|
options.SubscriptionDetails.Items.Add(
|
||||||
{
|
new() { Quantity = parameters.PasswordManager.Seats, Plan = plan.PasswordManager.StripeSeatPlanId }
|
||||||
Quantity = parameters.SecretsManager?.Seats ?? 0,
|
);
|
||||||
Plan = plan.SecretsManager.StripeSeatPlanId
|
}
|
||||||
});
|
else
|
||||||
|
{
|
||||||
|
options.SubscriptionDetails.Items.Add(
|
||||||
|
new() { Quantity = 1, Plan = plan.PasswordManager.StripePlanId }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plan.SecretsManager.HasAdditionalServiceAccountOption)
|
if (plan.SupportsSecretsManager)
|
||||||
{
|
{
|
||||||
options.SubscriptionDetails.Items.Add(new()
|
if (plan.SecretsManager.HasAdditionalSeatsOption)
|
||||||
{
|
{
|
||||||
Quantity = parameters.SecretsManager?.AdditionalMachineAccounts ?? 0,
|
options.SubscriptionDetails.Items.Add(new()
|
||||||
Plan = plan.SecretsManager.StripeServiceAccountPlanId
|
{
|
||||||
});
|
Quantity = parameters.SecretsManager?.Seats ?? 0,
|
||||||
|
Plan = plan.SecretsManager.StripeSeatPlanId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.SecretsManager.HasAdditionalServiceAccountOption)
|
||||||
|
{
|
||||||
|
options.SubscriptionDetails.Items.Add(new()
|
||||||
|
{
|
||||||
|
Quantity = parameters.SecretsManager?.AdditionalMachineAccounts ?? 0,
|
||||||
|
Plan = plan.SecretsManager.StripeServiceAccountPlanId
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1420,7 +1423,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options);
|
var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options);
|
||||||
|
|
||||||
var effectiveTaxRate = invoice.Tax != null && invoice.TotalExcludingTax != null
|
var effectiveTaxRate = invoice.Tax != null && invoice.TotalExcludingTax != null && invoice.TotalExcludingTax.Value != 0
|
||||||
? invoice.Tax.Value.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor()
|
? invoice.Tax.Value.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor()
|
||||||
: 0M;
|
: 0M;
|
||||||
|
|
||||||
|
@ -1003,7 +1003,7 @@ public class CipherService : ICipherService
|
|||||||
|
|
||||||
private async Task ValidateViewPasswordUserAsync(Cipher cipher)
|
private async Task ValidateViewPasswordUserAsync(Cipher cipher)
|
||||||
{
|
{
|
||||||
if (cipher.Type != CipherType.Login || cipher.Data == null || !cipher.OrganizationId.HasValue)
|
if (cipher.Data == null || !cipher.OrganizationId.HasValue)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1014,21 +1014,58 @@ public class CipherService : ICipherService
|
|||||||
// Check if user is a "hidden password" user
|
// Check if user is a "hidden password" user
|
||||||
if (!cipherPermissions.TryGetValue(cipher.Id, out var permission) || !(permission.ViewPassword && permission.Edit))
|
if (!cipherPermissions.TryGetValue(cipher.Id, out var permission) || !(permission.ViewPassword && permission.Edit))
|
||||||
{
|
{
|
||||||
|
var existingCipherData = DeserializeCipherData(existingCipher);
|
||||||
|
var newCipherData = DeserializeCipherData(cipher);
|
||||||
|
|
||||||
// "hidden password" users may not add cipher key encryption
|
// "hidden password" users may not add cipher key encryption
|
||||||
if (existingCipher.Key == null && cipher.Key != null)
|
if (existingCipher.Key == null && cipher.Key != null)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("You do not have permission to add cipher key encryption.");
|
throw new BadRequestException("You do not have permission to add cipher key encryption.");
|
||||||
}
|
}
|
||||||
// "hidden password" users may not change passwords, TOTP codes, or passkeys, so we need to set them back to the original values
|
// Keep only non-hidden fileds from the new cipher
|
||||||
var existingCipherData = JsonSerializer.Deserialize<CipherLoginData>(existingCipher.Data);
|
var nonHiddenFields = newCipherData.Fields?.Where(f => f.Type != FieldType.Hidden) ?? [];
|
||||||
var newCipherData = JsonSerializer.Deserialize<CipherLoginData>(cipher.Data);
|
// Get hidden fields from the existing cipher
|
||||||
newCipherData.Fido2Credentials = existingCipherData.Fido2Credentials;
|
var hiddenFields = existingCipherData.Fields?.Where(f => f.Type == FieldType.Hidden) ?? [];
|
||||||
newCipherData.Totp = existingCipherData.Totp;
|
// Replace the hidden fields in new cipher data with the existing ones
|
||||||
newCipherData.Password = existingCipherData.Password;
|
newCipherData.Fields = nonHiddenFields.Concat(hiddenFields);
|
||||||
cipher.Data = JsonSerializer.Serialize(newCipherData);
|
cipher.Data = SerializeCipherData(newCipherData);
|
||||||
|
if (existingCipherData is CipherLoginData existingLoginData && newCipherData is CipherLoginData newLoginCipherData)
|
||||||
|
{
|
||||||
|
// "hidden password" users may not change passwords, TOTP codes, or passkeys, so we need to set them back to the original values
|
||||||
|
newLoginCipherData.Fido2Credentials = existingLoginData.Fido2Credentials;
|
||||||
|
newLoginCipherData.Totp = existingLoginData.Totp;
|
||||||
|
newLoginCipherData.Password = existingLoginData.Password;
|
||||||
|
cipher.Data = SerializeCipherData(newLoginCipherData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string SerializeCipherData(CipherData data)
|
||||||
|
{
|
||||||
|
return data switch
|
||||||
|
{
|
||||||
|
CipherLoginData loginData => JsonSerializer.Serialize(loginData),
|
||||||
|
CipherIdentityData identityData => JsonSerializer.Serialize(identityData),
|
||||||
|
CipherCardData cardData => JsonSerializer.Serialize(cardData),
|
||||||
|
CipherSecureNoteData noteData => JsonSerializer.Serialize(noteData),
|
||||||
|
CipherSSHKeyData sshKeyData => JsonSerializer.Serialize(sshKeyData),
|
||||||
|
_ => throw new ArgumentException("Unsupported cipher data type.", nameof(data))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private CipherData DeserializeCipherData(Cipher cipher)
|
||||||
|
{
|
||||||
|
return cipher.Type switch
|
||||||
|
{
|
||||||
|
CipherType.Login => JsonSerializer.Deserialize<CipherLoginData>(cipher.Data),
|
||||||
|
CipherType.Identity => JsonSerializer.Deserialize<CipherIdentityData>(cipher.Data),
|
||||||
|
CipherType.Card => JsonSerializer.Deserialize<CipherCardData>(cipher.Data),
|
||||||
|
CipherType.SecureNote => JsonSerializer.Deserialize<CipherSecureNoteData>(cipher.Data),
|
||||||
|
CipherType.SSHKey => JsonSerializer.Deserialize<CipherSSHKeyData>(cipher.Data),
|
||||||
|
_ => throw new ArgumentException("Unsupported cipher type.", nameof(cipher))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// This method is used to filter ciphers based on the user's permissions to delete them.
|
// This method is used to filter ciphers based on the user's permissions to delete them.
|
||||||
// It supports both the old and new logic depending on the feature flag.
|
// It supports both the old and new logic depending on the feature flag.
|
||||||
private async Task<List<T>> FilterCiphersByDeletePermission<T>(
|
private async Task<List<T>> FilterCiphersByDeletePermission<T>(
|
||||||
|
@ -8,7 +8,6 @@ using Bit.Core.Auth.Models.Business.Tokenables;
|
|||||||
using Bit.Core.Auth.Services;
|
using Bit.Core.Auth.Services;
|
||||||
using Bit.Core.Auth.UserFeatures.Registration;
|
using Bit.Core.Auth.UserFeatures.Registration;
|
||||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||||
using Bit.Core.Auth.Utilities;
|
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -114,16 +113,6 @@ public class AccountsController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("register")]
|
|
||||||
[CaptchaProtected]
|
|
||||||
public async Task<RegisterResponseModel> PostRegister([FromBody] RegisterRequestModel model)
|
|
||||||
{
|
|
||||||
var user = model.ToUser();
|
|
||||||
var identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash,
|
|
||||||
model.Token, model.OrganizationUserId);
|
|
||||||
return ProcessRegistrationResult(identityResult, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("register/send-verification-email")]
|
[HttpPost("register/send-verification-email")]
|
||||||
public async Task<IActionResult> PostRegisterSendVerificationEmail([FromBody] RegisterSendVerificationEmailRequestModel model)
|
public async Task<IActionResult> PostRegisterSendVerificationEmail([FromBody] RegisterSendVerificationEmailRequestModel model)
|
||||||
{
|
{
|
||||||
@ -175,8 +164,6 @@ public class AccountsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("register/finish")]
|
[HttpPost("register/finish")]
|
||||||
@ -185,7 +172,6 @@ public class AccountsController : Controller
|
|||||||
var user = model.ToUser();
|
var user = model.ToUser();
|
||||||
|
|
||||||
// Users will either have an emailed token or an email verification token - not both.
|
// Users will either have an emailed token or an email verification token - not both.
|
||||||
|
|
||||||
IdentityResult identityResult = null;
|
IdentityResult identityResult = null;
|
||||||
|
|
||||||
switch (model.GetTokenType())
|
switch (model.GetTokenType())
|
||||||
@ -196,33 +182,27 @@ public class AccountsController : Controller
|
|||||||
model.EmailVerificationToken);
|
model.EmailVerificationToken);
|
||||||
|
|
||||||
return ProcessRegistrationResult(identityResult, user);
|
return ProcessRegistrationResult(identityResult, user);
|
||||||
break;
|
|
||||||
case RegisterFinishTokenType.OrganizationInvite:
|
case RegisterFinishTokenType.OrganizationInvite:
|
||||||
identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash,
|
identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash,
|
||||||
model.OrgInviteToken, model.OrganizationUserId);
|
model.OrgInviteToken, model.OrganizationUserId);
|
||||||
|
|
||||||
return ProcessRegistrationResult(identityResult, user);
|
return ProcessRegistrationResult(identityResult, user);
|
||||||
break;
|
|
||||||
case RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan:
|
case RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan:
|
||||||
identityResult = await _registerUserCommand.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, model.MasterPasswordHash, model.OrgSponsoredFreeFamilyPlanToken);
|
identityResult = await _registerUserCommand.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, model.MasterPasswordHash, model.OrgSponsoredFreeFamilyPlanToken);
|
||||||
|
|
||||||
return ProcessRegistrationResult(identityResult, user);
|
return ProcessRegistrationResult(identityResult, user);
|
||||||
break;
|
|
||||||
case RegisterFinishTokenType.EmergencyAccessInvite:
|
case RegisterFinishTokenType.EmergencyAccessInvite:
|
||||||
Debug.Assert(model.AcceptEmergencyAccessId.HasValue);
|
Debug.Assert(model.AcceptEmergencyAccessId.HasValue);
|
||||||
identityResult = await _registerUserCommand.RegisterUserViaAcceptEmergencyAccessInviteToken(user, model.MasterPasswordHash,
|
identityResult = await _registerUserCommand.RegisterUserViaAcceptEmergencyAccessInviteToken(user, model.MasterPasswordHash,
|
||||||
model.AcceptEmergencyAccessInviteToken, model.AcceptEmergencyAccessId.Value);
|
model.AcceptEmergencyAccessInviteToken, model.AcceptEmergencyAccessId.Value);
|
||||||
|
|
||||||
return ProcessRegistrationResult(identityResult, user);
|
return ProcessRegistrationResult(identityResult, user);
|
||||||
break;
|
|
||||||
case RegisterFinishTokenType.ProviderInvite:
|
case RegisterFinishTokenType.ProviderInvite:
|
||||||
Debug.Assert(model.ProviderUserId.HasValue);
|
Debug.Assert(model.ProviderUserId.HasValue);
|
||||||
identityResult = await _registerUserCommand.RegisterUserViaProviderInviteToken(user, model.MasterPasswordHash,
|
identityResult = await _registerUserCommand.RegisterUserViaProviderInviteToken(user, model.MasterPasswordHash,
|
||||||
model.ProviderInviteToken, model.ProviderUserId.Value);
|
model.ProviderInviteToken, model.ProviderUserId.Value);
|
||||||
|
|
||||||
return ProcessRegistrationResult(identityResult, user);
|
return ProcessRegistrationResult(identityResult, user);
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new BadRequestException("Invalid registration finish request");
|
throw new BadRequestException("Invalid registration finish request");
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,6 @@
|
|||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using Bit.Api.IntegrationTest.Factories;
|
using Bit.Api.IntegrationTest.Factories;
|
||||||
using Bit.Api.IntegrationTest.Helpers;
|
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.Billing.Enums;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Data;
|
|
||||||
using Bit.Core.Services;
|
|
||||||
using NSubstitute;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Api.IntegrationTest.Controllers;
|
namespace Bit.Api.IntegrationTest.Controllers;
|
||||||
@ -19,7 +12,7 @@ public class AccountsControllerTest : IClassFixture<ApiApplicationFactory>
|
|||||||
public AccountsControllerTest(ApiApplicationFactory factory) => _factory = factory;
|
public AccountsControllerTest(ApiApplicationFactory factory) => _factory = factory;
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetPublicKey()
|
public async Task GetAccountsProfile_success()
|
||||||
{
|
{
|
||||||
var tokens = await _factory.LoginWithNewAccount();
|
var tokens = await _factory.LoginWithNewAccount();
|
||||||
var client = _factory.CreateClient();
|
var client = _factory.CreateClient();
|
||||||
@ -33,36 +26,13 @@ public class AccountsControllerTest : IClassFixture<ApiApplicationFactory>
|
|||||||
var content = await response.Content.ReadFromJsonAsync<ProfileResponseModel>();
|
var content = await response.Content.ReadFromJsonAsync<ProfileResponseModel>();
|
||||||
Assert.NotNull(content);
|
Assert.NotNull(content);
|
||||||
Assert.Equal("integration-test@bitwarden.com", content.Email);
|
Assert.Equal("integration-test@bitwarden.com", content.Email);
|
||||||
Assert.Null(content.Name);
|
Assert.NotNull(content.Name);
|
||||||
Assert.False(content.EmailVerified);
|
Assert.True(content.EmailVerified);
|
||||||
Assert.False(content.Premium);
|
Assert.False(content.Premium);
|
||||||
Assert.False(content.PremiumFromOrganization);
|
Assert.False(content.PremiumFromOrganization);
|
||||||
Assert.Equal("en-US", content.Culture);
|
Assert.Equal("en-US", content.Culture);
|
||||||
Assert.Null(content.Key);
|
Assert.NotNull(content.Key);
|
||||||
Assert.Null(content.PrivateKey);
|
Assert.NotNull(content.PrivateKey);
|
||||||
Assert.NotNull(content.SecurityStamp);
|
Assert.NotNull(content.SecurityStamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> SetupOrganizationManagedAccount()
|
|
||||||
{
|
|
||||||
_factory.SubstituteService<IFeatureService>(featureService =>
|
|
||||||
featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true));
|
|
||||||
|
|
||||||
// Create the owner account
|
|
||||||
var ownerEmail = $"{Guid.NewGuid()}@bitwarden.com";
|
|
||||||
await _factory.LoginWithNewAccount(ownerEmail);
|
|
||||||
|
|
||||||
// Create the organization
|
|
||||||
var (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
|
|
||||||
ownerEmail: ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
|
|
||||||
|
|
||||||
// Create a new organization member
|
|
||||||
var (email, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
|
||||||
OrganizationUserType.Custom, new Permissions { AccessReports = true, ManageScim = true });
|
|
||||||
|
|
||||||
// Add a verified domain
|
|
||||||
await OrganizationTestHelpers.CreateVerifiedDomainAsync(_factory, _organization.Id, "bitwarden.com");
|
|
||||||
|
|
||||||
return email;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using Bit.Identity.Models.Request.Accounts;
|
using Bit.Core;
|
||||||
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.IntegrationTestCommon.Factories;
|
using Bit.IntegrationTestCommon.Factories;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.TestHost;
|
using Microsoft.AspNetCore.TestHost;
|
||||||
@ -42,13 +44,23 @@ public class ApiApplicationFactory : WebApplicationFactoryBase<Startup>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Helper for registering and logging in to a new account
|
/// Helper for registering and logging in to a new account
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<(string Token, string RefreshToken)> LoginWithNewAccount(string email = "integration-test@bitwarden.com", string masterPasswordHash = "master_password_hash")
|
public async Task<(string Token, string RefreshToken)> LoginWithNewAccount(
|
||||||
|
string email = "integration-test@bitwarden.com", string masterPasswordHash = "master_password_hash")
|
||||||
{
|
{
|
||||||
await _identityApplicationFactory.RegisterAsync(new RegisterRequestModel
|
await _identityApplicationFactory.RegisterNewIdentityFactoryUserAsync(
|
||||||
{
|
new RegisterFinishRequestModel
|
||||||
Email = email,
|
{
|
||||||
MasterPasswordHash = masterPasswordHash,
|
Email = email,
|
||||||
});
|
MasterPasswordHash = masterPasswordHash,
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||||
|
UserAsymmetricKeys = new KeysRequestModel()
|
||||||
|
{
|
||||||
|
PublicKey = "public_key",
|
||||||
|
EncryptedPrivateKey = "private_key"
|
||||||
|
},
|
||||||
|
UserSymmetricKey = "sym_key",
|
||||||
|
});
|
||||||
|
|
||||||
return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash);
|
return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash);
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,8 @@ public static class OrganizationTestHelpers
|
|||||||
string userEmail,
|
string userEmail,
|
||||||
OrganizationUserType type,
|
OrganizationUserType type,
|
||||||
bool accessSecretsManager = false,
|
bool accessSecretsManager = false,
|
||||||
Permissions? permissions = null
|
Permissions? permissions = null,
|
||||||
|
OrganizationUserStatusType userStatusType = OrganizationUserStatusType.Confirmed
|
||||||
) where T : class
|
) where T : class
|
||||||
{
|
{
|
||||||
var userRepository = factory.GetService<IUserRepository>();
|
var userRepository = factory.GetService<IUserRepository>();
|
||||||
@ -74,7 +75,7 @@ public static class OrganizationTestHelpers
|
|||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
Key = null,
|
Key = null,
|
||||||
Type = type,
|
Type = type,
|
||||||
Status = OrganizationUserStatusType.Confirmed,
|
Status = userStatusType,
|
||||||
ExternalId = null,
|
ExternalId = null,
|
||||||
AccessSecretsManager = accessSecretsManager,
|
AccessSecretsManager = accessSecretsManager,
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Net;
|
#nullable enable
|
||||||
|
using System.Net;
|
||||||
using Bit.Api.IntegrationTest.Factories;
|
using Bit.Api.IntegrationTest.Factories;
|
||||||
using Bit.Api.IntegrationTest.Helpers;
|
using Bit.Api.IntegrationTest.Helpers;
|
||||||
using Bit.Api.KeyManagement.Models.Requests;
|
using Bit.Api.KeyManagement.Models.Requests;
|
||||||
@ -7,6 +8,7 @@ using Bit.Api.Vault.Models;
|
|||||||
using Bit.Api.Vault.Models.Request;
|
using Bit.Api.Vault.Models.Request;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -31,6 +33,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
|||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly IDeviceRepository _deviceRepository;
|
private readonly IDeviceRepository _deviceRepository;
|
||||||
private readonly IPasswordHasher<User> _passwordHasher;
|
private readonly IPasswordHasher<User> _passwordHasher;
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private string _ownerEmail = null!;
|
private string _ownerEmail = null!;
|
||||||
|
|
||||||
public AccountsKeyManagementControllerTests(ApiApplicationFactory factory)
|
public AccountsKeyManagementControllerTests(ApiApplicationFactory factory)
|
||||||
@ -45,6 +48,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
|||||||
_emergencyAccessRepository = _factory.GetService<IEmergencyAccessRepository>();
|
_emergencyAccessRepository = _factory.GetService<IEmergencyAccessRepository>();
|
||||||
_organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
_organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||||
_passwordHasher = _factory.GetService<IPasswordHasher<User>>();
|
_passwordHasher = _factory.GetService<IPasswordHasher<User>>();
|
||||||
|
_organizationRepository = _factory.GetService<IOrganizationRepository>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
@ -174,7 +178,8 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task RotateUserAccountKeysAsync_NotLoggedIn_Unauthorized(RotateUserAccountKeysAndDataRequestModel request)
|
public async Task RotateUserAccountKeysAsync_NotLoggedIn_Unauthorized(
|
||||||
|
RotateUserAccountKeysAndDataRequestModel request)
|
||||||
{
|
{
|
||||||
var response = await _client.PostAsJsonAsync("/accounts/key-management/rotate-user-account-keys", request);
|
var response = await _client.PostAsJsonAsync("/accounts/key-management/rotate-user-account-keys", request);
|
||||||
|
|
||||||
@ -256,4 +261,97 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
|||||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory, userNewState.KdfMemory);
|
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory, userNewState.KdfMemory);
|
||||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism, userNewState.KdfParallelism);
|
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism, userNewState.KdfParallelism);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostSetKeyConnectorKeyAsync_NotLoggedIn_Unauthorized(SetKeyConnectorKeyRequestModel request)
|
||||||
|
{
|
||||||
|
var response = await _client.PostAsJsonAsync("/accounts/set-key-connector-key", request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostSetKeyConnectorKeyAsync_Success(string organizationSsoIdentifier,
|
||||||
|
SetKeyConnectorKeyRequestModel request)
|
||||||
|
{
|
||||||
|
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
|
||||||
|
PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10,
|
||||||
|
paymentMethod: PaymentMethodType.Card);
|
||||||
|
organization.UseKeyConnector = true;
|
||||||
|
organization.UseSso = true;
|
||||||
|
organization.Identifier = organizationSsoIdentifier;
|
||||||
|
await _organizationRepository.ReplaceAsync(organization);
|
||||||
|
|
||||||
|
var ssoUserEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
await _factory.LoginWithNewAccount(ssoUserEmail);
|
||||||
|
await _loginHelper.LoginAsync(ssoUserEmail);
|
||||||
|
|
||||||
|
await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, ssoUserEmail,
|
||||||
|
OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Invited);
|
||||||
|
|
||||||
|
var ssoUser = await _userRepository.GetByEmailAsync(ssoUserEmail);
|
||||||
|
Assert.NotNull(ssoUser);
|
||||||
|
|
||||||
|
request.Keys = new KeysRequestModel
|
||||||
|
{
|
||||||
|
PublicKey = ssoUser.PublicKey,
|
||||||
|
EncryptedPrivateKey = ssoUser.PrivateKey
|
||||||
|
};
|
||||||
|
request.Key = _mockEncryptedString;
|
||||||
|
request.OrgIdentifier = organizationSsoIdentifier;
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync("/accounts/set-key-connector-key", request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var user = await _userRepository.GetByEmailAsync(ssoUserEmail);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
Assert.Equal(request.Key, user.Key);
|
||||||
|
Assert.True(user.UsesKeyConnector);
|
||||||
|
Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));
|
||||||
|
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
|
||||||
|
var ssoOrganizationUser =
|
||||||
|
await _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id);
|
||||||
|
Assert.NotNull(ssoOrganizationUser);
|
||||||
|
Assert.Equal(OrganizationUserStatusType.Accepted, ssoOrganizationUser.Status);
|
||||||
|
Assert.Equal(user.Id, ssoOrganizationUser.UserId);
|
||||||
|
Assert.Null(ssoOrganizationUser.Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostConvertToKeyConnectorAsync_NotLoggedIn_Unauthorized()
|
||||||
|
{
|
||||||
|
var response = await _client.PostAsJsonAsync("/accounts/convert-to-key-connector", new { });
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostConvertToKeyConnectorAsync_Success()
|
||||||
|
{
|
||||||
|
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
|
||||||
|
PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10,
|
||||||
|
paymentMethod: PaymentMethodType.Card);
|
||||||
|
organization.UseKeyConnector = true;
|
||||||
|
organization.UseSso = true;
|
||||||
|
await _organizationRepository.ReplaceAsync(organization);
|
||||||
|
|
||||||
|
var ssoUserEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
await _factory.LoginWithNewAccount(ssoUserEmail);
|
||||||
|
await _loginHelper.LoginAsync(ssoUserEmail);
|
||||||
|
|
||||||
|
await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, ssoUserEmail,
|
||||||
|
OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Accepted);
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync("/accounts/convert-to-key-connector", new { });
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var user = await _userRepository.GetByEmailAsync(ssoUserEmail);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
Assert.Null(user.MasterPassword);
|
||||||
|
Assert.True(user.UsesKeyConnector);
|
||||||
|
Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));
|
||||||
|
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -178,4 +178,133 @@ public class AccountsKeyManagementControllerTests
|
|||||||
Assert.NotEmpty(ex.ModelState.Values);
|
Assert.NotEmpty(ex.ModelState.Values);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostSetKeyConnectorKeyAsync_UserNull_Throws(
|
||||||
|
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||||
|
SetKeyConnectorKeyRequestModel data)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(data));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserService>().ReceivedWithAnyArgs(0)
|
||||||
|
.SetKeyConnectorKeyAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostSetKeyConnectorKeyAsync_SetKeyConnectorKeyFails_ThrowsBadRequestWithErrorResponse(
|
||||||
|
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||||
|
SetKeyConnectorKeyRequestModel data, User expectedUser)
|
||||||
|
{
|
||||||
|
expectedUser.PublicKey = null;
|
||||||
|
expectedUser.PrivateKey = null;
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||||
|
.Returns(expectedUser);
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.SetKeyConnectorKeyAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>())
|
||||||
|
.Returns(IdentityResult.Failed(new IdentityError { Description = "set key connector key error" }));
|
||||||
|
|
||||||
|
var badRequestException =
|
||||||
|
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(data));
|
||||||
|
|
||||||
|
Assert.Equal(1, badRequestException.ModelState.ErrorCount);
|
||||||
|
Assert.Equal("set key connector key error", badRequestException.ModelState.Root.Errors[0].ErrorMessage);
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(1)
|
||||||
|
.SetKeyConnectorKeyAsync(Arg.Do<User>(user =>
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedUser.Id, user.Id);
|
||||||
|
Assert.Equal(data.Key, user.Key);
|
||||||
|
Assert.Equal(data.Kdf, user.Kdf);
|
||||||
|
Assert.Equal(data.KdfIterations, user.KdfIterations);
|
||||||
|
Assert.Equal(data.KdfMemory, user.KdfMemory);
|
||||||
|
Assert.Equal(data.KdfParallelism, user.KdfParallelism);
|
||||||
|
Assert.Equal(data.Keys.PublicKey, user.PublicKey);
|
||||||
|
Assert.Equal(data.Keys.EncryptedPrivateKey, user.PrivateKey);
|
||||||
|
}), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostSetKeyConnectorKeyAsync_SetKeyConnectorKeySucceeds_OkResponse(
|
||||||
|
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||||
|
SetKeyConnectorKeyRequestModel data, User expectedUser)
|
||||||
|
{
|
||||||
|
expectedUser.PublicKey = null;
|
||||||
|
expectedUser.PrivateKey = null;
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||||
|
.Returns(expectedUser);
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.SetKeyConnectorKeyAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>())
|
||||||
|
.Returns(IdentityResult.Success);
|
||||||
|
|
||||||
|
await sutProvider.Sut.PostSetKeyConnectorKeyAsync(data);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(1)
|
||||||
|
.SetKeyConnectorKeyAsync(Arg.Do<User>(user =>
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedUser.Id, user.Id);
|
||||||
|
Assert.Equal(data.Key, user.Key);
|
||||||
|
Assert.Equal(data.Kdf, user.Kdf);
|
||||||
|
Assert.Equal(data.KdfIterations, user.KdfIterations);
|
||||||
|
Assert.Equal(data.KdfMemory, user.KdfMemory);
|
||||||
|
Assert.Equal(data.KdfParallelism, user.KdfParallelism);
|
||||||
|
Assert.Equal(data.Keys.PublicKey, user.PublicKey);
|
||||||
|
Assert.Equal(data.Keys.EncryptedPrivateKey, user.PrivateKey);
|
||||||
|
}), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostConvertToKeyConnectorAsync_UserNull_Throws(
|
||||||
|
SutProvider<AccountsKeyManagementController> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostConvertToKeyConnectorAsync());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserService>().ReceivedWithAnyArgs(0)
|
||||||
|
.ConvertToKeyConnectorAsync(Arg.Any<User>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostConvertToKeyConnectorAsync_ConvertToKeyConnectorFails_ThrowsBadRequestWithErrorResponse(
|
||||||
|
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||||
|
User expectedUser)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||||
|
.Returns(expectedUser);
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.ConvertToKeyConnectorAsync(Arg.Any<User>())
|
||||||
|
.Returns(IdentityResult.Failed(new IdentityError { Description = "convert to key connector error" }));
|
||||||
|
|
||||||
|
var badRequestException =
|
||||||
|
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostConvertToKeyConnectorAsync());
|
||||||
|
|
||||||
|
Assert.Equal(1, badRequestException.ModelState.ErrorCount);
|
||||||
|
Assert.Equal("convert to key connector error", badRequestException.ModelState.Root.Errors[0].ErrorMessage);
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(1)
|
||||||
|
.ConvertToKeyConnectorAsync(Arg.Is(expectedUser));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostConvertToKeyConnectorAsync_ConvertToKeyConnectorSucceeds_OkResponse(
|
||||||
|
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||||
|
User expectedUser)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||||
|
.Returns(expectedUser);
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.ConvertToKeyConnectorAsync(Arg.Any<User>())
|
||||||
|
.Returns(IdentityResult.Success);
|
||||||
|
|
||||||
|
await sutProvider.Sut.PostConvertToKeyConnectorAsync();
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserService>().Received(1)
|
||||||
|
.ConvertToKeyConnectorAsync(Arg.Is(expectedUser));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -287,6 +287,77 @@ public class InviteOrganizationUserCommandTests
|
|||||||
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == ownerDetails.Email)));
|
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == ownerDetails.Email)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task InviteScimOrganizationUserAsync_WhenValidInviteCausesOrgToAutoscale_ThenOrganizationOwnersShouldBeNotified(
|
||||||
|
MailAddress address,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser user,
|
||||||
|
FakeTimeProvider timeProvider,
|
||||||
|
string externalId,
|
||||||
|
OrganizationUserUserDetails ownerDetails,
|
||||||
|
SutProvider<InviteOrganizationUsersCommand> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Email = address.Address;
|
||||||
|
organization.Seats = 1;
|
||||||
|
organization.MaxAutoscaleSeats = 2;
|
||||||
|
organization.OwnersNotifiedOfAutoscaling = null;
|
||||||
|
ownerDetails.Type = OrganizationUserType.Owner;
|
||||||
|
|
||||||
|
var inviteOrganization = new InviteOrganization(organization, new Enterprise2019Plan(true));
|
||||||
|
|
||||||
|
var request = new InviteOrganizationUsersRequest(
|
||||||
|
invites:
|
||||||
|
[
|
||||||
|
new OrganizationUserInvite(
|
||||||
|
email: user.Email,
|
||||||
|
assignedCollections: [],
|
||||||
|
groups: [],
|
||||||
|
type: OrganizationUserType.User,
|
||||||
|
permissions: new Permissions(),
|
||||||
|
externalId: externalId,
|
||||||
|
accessSecretsManager: true)
|
||||||
|
],
|
||||||
|
inviteOrganization: inviteOrganization,
|
||||||
|
performedBy: Guid.Empty,
|
||||||
|
timeProvider.GetUtcNow());
|
||||||
|
|
||||||
|
var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
orgUserRepository
|
||||||
|
.SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)
|
||||||
|
.Returns([]);
|
||||||
|
orgUserRepository
|
||||||
|
.GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)
|
||||||
|
.Returns([ownerDetails]);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IInviteUsersValidator>()
|
||||||
|
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
|
||||||
|
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(
|
||||||
|
GetInviteValidationRequestMock(request, inviteOrganization, organization)
|
||||||
|
.WithPasswordManagerUpdate(
|
||||||
|
new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
|
||||||
|
|
||||||
|
Assert.NotNull(inviteOrganization.MaxAutoScaleSeats);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IMailService>()
|
||||||
|
.Received(1)
|
||||||
|
.SendOrganizationAutoscaledEmailAsync(organization,
|
||||||
|
inviteOrganization.Seats.Value,
|
||||||
|
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == ownerDetails.Email)));
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task InviteScimOrganizationUserAsync_WhenValidInviteIncreasesSeats_ThenSeatTotalShouldBeUpdated(
|
public async Task InviteScimOrganizationUserAsync_WhenValidInviteIncreasesSeats_ThenSeatTotalShouldBeUpdated(
|
||||||
@ -610,4 +681,237 @@ public class InviteOrganizationUserCommandTests
|
|||||||
.SendOrganizationMaxSeatLimitReachedEmailAsync(organization, 2,
|
.SendOrganizationMaxSeatLimitReachedEmailAsync(organization, 2,
|
||||||
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == "provider@email.com")));
|
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == "provider@email.com")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task InviteScimOrganizationUserAsync_WhenAnOrganizationIsManagedByAProviderAndAutoscaleOccurs_ThenAnEmailShouldBeSentToTheProvider(
|
||||||
|
MailAddress address,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser user,
|
||||||
|
FakeTimeProvider timeProvider,
|
||||||
|
string externalId,
|
||||||
|
OrganizationUserUserDetails ownerDetails,
|
||||||
|
ProviderOrganization providerOrganization,
|
||||||
|
SutProvider<InviteOrganizationUsersCommand> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Email = address.Address;
|
||||||
|
organization.Seats = 1;
|
||||||
|
organization.SmSeats = 1;
|
||||||
|
organization.MaxAutoscaleSeats = 2;
|
||||||
|
organization.MaxAutoscaleSmSeats = 2;
|
||||||
|
organization.OwnersNotifiedOfAutoscaling = null;
|
||||||
|
ownerDetails.Type = OrganizationUserType.Owner;
|
||||||
|
|
||||||
|
providerOrganization.OrganizationId = organization.Id;
|
||||||
|
|
||||||
|
var inviteOrganization = new InviteOrganization(organization, new Enterprise2019Plan(true));
|
||||||
|
|
||||||
|
var request = new InviteOrganizationUsersRequest(
|
||||||
|
invites: [
|
||||||
|
new OrganizationUserInvite(
|
||||||
|
email: user.Email,
|
||||||
|
assignedCollections: [],
|
||||||
|
groups: [],
|
||||||
|
type: OrganizationUserType.User,
|
||||||
|
permissions: new Permissions(),
|
||||||
|
externalId: externalId,
|
||||||
|
accessSecretsManager: true)
|
||||||
|
],
|
||||||
|
inviteOrganization: inviteOrganization,
|
||||||
|
performedBy: Guid.Empty,
|
||||||
|
timeProvider.GetUtcNow());
|
||||||
|
|
||||||
|
var secretsManagerSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, inviteOrganization.Plan, true)
|
||||||
|
.AdjustSeats(request.Invites.Count(x => x.AccessSecretsManager));
|
||||||
|
|
||||||
|
var passwordManagerSubscriptionUpdate =
|
||||||
|
new PasswordManagerSubscriptionUpdate(inviteOrganization, 1, request.Invites.Length);
|
||||||
|
|
||||||
|
var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
orgUserRepository
|
||||||
|
.SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)
|
||||||
|
.Returns([]);
|
||||||
|
orgUserRepository
|
||||||
|
.GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)
|
||||||
|
.Returns([ownerDetails]);
|
||||||
|
|
||||||
|
var orgRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
|
||||||
|
orgRepository.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IInviteUsersValidator>()
|
||||||
|
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
|
||||||
|
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)
|
||||||
|
.WithPasswordManagerUpdate(passwordManagerSubscriptionUpdate)
|
||||||
|
.WithSecretsManagerUpdate(secretsManagerSubscriptionUpdate)));
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderOrganizationRepository>()
|
||||||
|
.GetByOrganizationId(organization.Id)
|
||||||
|
.Returns(providerOrganization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderUserRepository>()
|
||||||
|
.GetManyDetailsByProviderAsync(providerOrganization.ProviderId, ProviderUserStatusType.Confirmed)
|
||||||
|
.Returns(new List<ProviderUserUserDetails>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Email = "provider@email.com"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IMailService>().Received(1)
|
||||||
|
.SendOrganizationAutoscaledEmailAsync(organization, 1,
|
||||||
|
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == "provider@email.com")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task InviteScimOrganizationUserAsync_WhenAnOrganizationAutoscalesButOwnersHaveAlreadyBeenNotified_ThenAnEmailShouldNotBeSent(
|
||||||
|
MailAddress address,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser user,
|
||||||
|
FakeTimeProvider timeProvider,
|
||||||
|
string externalId,
|
||||||
|
OrganizationUserUserDetails ownerDetails,
|
||||||
|
SutProvider<InviteOrganizationUsersCommand> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Email = address.Address;
|
||||||
|
organization.Seats = 1;
|
||||||
|
organization.MaxAutoscaleSeats = 2;
|
||||||
|
organization.OwnersNotifiedOfAutoscaling = DateTime.UtcNow;
|
||||||
|
ownerDetails.Type = OrganizationUserType.Owner;
|
||||||
|
|
||||||
|
var inviteOrganization = new InviteOrganization(organization, new Enterprise2019Plan(true));
|
||||||
|
|
||||||
|
var request = new InviteOrganizationUsersRequest(
|
||||||
|
invites:
|
||||||
|
[
|
||||||
|
new OrganizationUserInvite(
|
||||||
|
email: user.Email,
|
||||||
|
assignedCollections: [],
|
||||||
|
groups: [],
|
||||||
|
type: OrganizationUserType.User,
|
||||||
|
permissions: new Permissions(),
|
||||||
|
externalId: externalId,
|
||||||
|
accessSecretsManager: true)
|
||||||
|
],
|
||||||
|
inviteOrganization: inviteOrganization,
|
||||||
|
performedBy: Guid.Empty,
|
||||||
|
timeProvider.GetUtcNow());
|
||||||
|
|
||||||
|
var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
orgUserRepository
|
||||||
|
.SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)
|
||||||
|
.Returns([]);
|
||||||
|
orgUserRepository
|
||||||
|
.GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)
|
||||||
|
.Returns([ownerDetails]);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IInviteUsersValidator>()
|
||||||
|
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
|
||||||
|
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(
|
||||||
|
GetInviteValidationRequestMock(request, inviteOrganization, organization)
|
||||||
|
.WithPasswordManagerUpdate(
|
||||||
|
new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
|
||||||
|
|
||||||
|
Assert.NotNull(inviteOrganization.MaxAutoScaleSeats);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IMailService>()
|
||||||
|
.DidNotReceive()
|
||||||
|
.SendOrganizationAutoscaledEmailAsync(Arg.Any<Organization>(),
|
||||||
|
Arg.Any<int>(),
|
||||||
|
Arg.Any<IEnumerable<string>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task InviteScimOrganizationUserAsync_WhenAnOrganizationDoesNotAutoScale_ThenAnEmailShouldNotBeSent(
|
||||||
|
MailAddress address,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser user,
|
||||||
|
FakeTimeProvider timeProvider,
|
||||||
|
string externalId,
|
||||||
|
OrganizationUserUserDetails ownerDetails,
|
||||||
|
SutProvider<InviteOrganizationUsersCommand> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Email = address.Address;
|
||||||
|
organization.Seats = 2;
|
||||||
|
organization.MaxAutoscaleSeats = 2;
|
||||||
|
organization.OwnersNotifiedOfAutoscaling = DateTime.UtcNow;
|
||||||
|
ownerDetails.Type = OrganizationUserType.Owner;
|
||||||
|
|
||||||
|
var inviteOrganization = new InviteOrganization(organization, new Enterprise2019Plan(true));
|
||||||
|
|
||||||
|
var request = new InviteOrganizationUsersRequest(
|
||||||
|
invites:
|
||||||
|
[
|
||||||
|
new OrganizationUserInvite(
|
||||||
|
email: user.Email,
|
||||||
|
assignedCollections: [],
|
||||||
|
groups: [],
|
||||||
|
type: OrganizationUserType.User,
|
||||||
|
permissions: new Permissions(),
|
||||||
|
externalId: externalId,
|
||||||
|
accessSecretsManager: true)
|
||||||
|
],
|
||||||
|
inviteOrganization: inviteOrganization,
|
||||||
|
performedBy: Guid.Empty,
|
||||||
|
timeProvider.GetUtcNow());
|
||||||
|
|
||||||
|
var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
orgUserRepository
|
||||||
|
.SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)
|
||||||
|
.Returns([]);
|
||||||
|
orgUserRepository
|
||||||
|
.GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)
|
||||||
|
.Returns([ownerDetails]);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IInviteUsersValidator>()
|
||||||
|
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
|
||||||
|
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(
|
||||||
|
GetInviteValidationRequestMock(request, inviteOrganization, organization)
|
||||||
|
.WithPasswordManagerUpdate(
|
||||||
|
new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
|
||||||
|
|
||||||
|
Assert.NotNull(inviteOrganization.MaxAutoScaleSeats);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IMailService>()
|
||||||
|
.DidNotReceive()
|
||||||
|
.SendOrganizationAutoscaledEmailAsync(Arg.Any<Organization>(),
|
||||||
|
Arg.Any<int>(),
|
||||||
|
Arg.Any<IEnumerable<string>>());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using AutoFixture;
|
||||||
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Auth.AutoFixture;
|
||||||
|
|
||||||
|
internal class RegisterFinishRequestModelCustomization : ICustomization
|
||||||
|
{
|
||||||
|
[StrictEmailAddress, StringLength(256)]
|
||||||
|
public required string Email { get; set; }
|
||||||
|
public required KdfType Kdf { get; set; }
|
||||||
|
public required int KdfIterations { get; set; }
|
||||||
|
public string? EmailVerificationToken { get; set; }
|
||||||
|
public string? OrgInviteToken { get; set; }
|
||||||
|
public string? OrgSponsoredFreeFamilyPlanToken { get; set; }
|
||||||
|
public string? AcceptEmergencyAccessInviteToken { get; set; }
|
||||||
|
public string? ProviderInviteToken { get; set; }
|
||||||
|
|
||||||
|
public void Customize(IFixture fixture)
|
||||||
|
{
|
||||||
|
fixture.Customize<RegisterFinishRequestModel>(composer => composer
|
||||||
|
.With(o => o.Email, Email)
|
||||||
|
.With(o => o.Kdf, Kdf)
|
||||||
|
.With(o => o.KdfIterations, KdfIterations)
|
||||||
|
.With(o => o.EmailVerificationToken, EmailVerificationToken)
|
||||||
|
.With(o => o.OrgInviteToken, OrgInviteToken)
|
||||||
|
.With(o => o.OrgSponsoredFreeFamilyPlanToken, OrgSponsoredFreeFamilyPlanToken)
|
||||||
|
.With(o => o.AcceptEmergencyAccessInviteToken, AcceptEmergencyAccessInviteToken)
|
||||||
|
.With(o => o.ProviderInviteToken, ProviderInviteToken));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RegisterFinishRequestModelCustomizeAttribute : BitCustomizeAttribute
|
||||||
|
{
|
||||||
|
public string _email { get; set; } = "{0}@email.com";
|
||||||
|
public KdfType _kdf { get; set; } = KdfType.PBKDF2_SHA256;
|
||||||
|
public int _kdfIterations { get; set; } = AuthConstants.PBKDF2_ITERATIONS.Default;
|
||||||
|
public string? _emailVerificationToken { get; set; }
|
||||||
|
public string? _orgInviteToken { get; set; }
|
||||||
|
public string? _orgSponsoredFreeFamilyPlanToken { get; set; }
|
||||||
|
public string? _acceptEmergencyAccessInviteToken { get; set; }
|
||||||
|
public string? _providerInviteToken { get; set; }
|
||||||
|
|
||||||
|
public override ICustomization GetCustomization() => new RegisterFinishRequestModelCustomization()
|
||||||
|
{
|
||||||
|
Email = _email,
|
||||||
|
Kdf = _kdf,
|
||||||
|
KdfIterations = _kdfIterations,
|
||||||
|
EmailVerificationToken = _emailVerificationToken,
|
||||||
|
OrgInviteToken = _orgInviteToken,
|
||||||
|
OrgSponsoredFreeFamilyPlanToken = _orgSponsoredFreeFamilyPlanToken,
|
||||||
|
AcceptEmergencyAccessInviteToken = _acceptEmergencyAccessInviteToken,
|
||||||
|
ProviderInviteToken = _providerInviteToken
|
||||||
|
};
|
||||||
|
}
|
227
test/Core.Test/Services/StripePaymentServiceTests.cs
Normal file
227
test/Core.Test/Services/StripePaymentServiceTests.cs
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Models.Api.Requests;
|
||||||
|
using Bit.Core.Billing.Models.Api.Requests.Organizations;
|
||||||
|
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||||
|
using Bit.Core.Billing.Pricing;
|
||||||
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Test.Billing.Stubs;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Stripe;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Services;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class StripePaymentServiceTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithoutAdditionalStorage(
|
||||||
|
SutProvider<StripePaymentService> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IAutomaticTaxFactory>()
|
||||||
|
.CreateAsync(Arg.Is<AutomaticTaxFactoryParameters>(p => p.PlanType == PlanType.FamiliesAnnually))
|
||||||
|
.Returns(new FakeAutomaticTaxStrategy(true));
|
||||||
|
|
||||||
|
var familiesPlan = new FamiliesPlan();
|
||||||
|
sutProvider.GetDependency<IPricingClient>()
|
||||||
|
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||||
|
.Returns(familiesPlan);
|
||||||
|
|
||||||
|
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||||
|
{
|
||||||
|
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||||
|
{
|
||||||
|
Plan = PlanType.FamiliesAnnually,
|
||||||
|
AdditionalStorage = 0
|
||||||
|
},
|
||||||
|
TaxInformation = new TaxInformationRequestModel
|
||||||
|
{
|
||||||
|
Country = "FR",
|
||||||
|
PostalCode = "12345"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>()
|
||||||
|
.InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(p =>
|
||||||
|
p.Currency == "usd" &&
|
||||||
|
p.SubscriptionDetails.Items.Any(x =>
|
||||||
|
x.Plan == familiesPlan.PasswordManager.StripePlanId &&
|
||||||
|
x.Quantity == 1) &&
|
||||||
|
p.SubscriptionDetails.Items.Any(x =>
|
||||||
|
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
|
||||||
|
x.Quantity == 0)))
|
||||||
|
.Returns(new Invoice
|
||||||
|
{
|
||||||
|
TotalExcludingTax = 4000,
|
||||||
|
Tax = 800,
|
||||||
|
Total = 4800
|
||||||
|
});
|
||||||
|
|
||||||
|
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||||
|
|
||||||
|
Assert.Equal(8M, actual.TaxAmount);
|
||||||
|
Assert.Equal(48M, actual.TotalAmount);
|
||||||
|
Assert.Equal(40M, actual.TaxableBaseAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithAdditionalStorage(
|
||||||
|
SutProvider<StripePaymentService> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IAutomaticTaxFactory>()
|
||||||
|
.CreateAsync(Arg.Is<AutomaticTaxFactoryParameters>(p => p.PlanType == PlanType.FamiliesAnnually))
|
||||||
|
.Returns(new FakeAutomaticTaxStrategy(true));
|
||||||
|
|
||||||
|
var familiesPlan = new FamiliesPlan();
|
||||||
|
sutProvider.GetDependency<IPricingClient>()
|
||||||
|
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||||
|
.Returns(familiesPlan);
|
||||||
|
|
||||||
|
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||||
|
{
|
||||||
|
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||||
|
{
|
||||||
|
Plan = PlanType.FamiliesAnnually,
|
||||||
|
AdditionalStorage = 1
|
||||||
|
},
|
||||||
|
TaxInformation = new TaxInformationRequestModel
|
||||||
|
{
|
||||||
|
Country = "FR",
|
||||||
|
PostalCode = "12345"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>()
|
||||||
|
.InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(p =>
|
||||||
|
p.Currency == "usd" &&
|
||||||
|
p.SubscriptionDetails.Items.Any(x =>
|
||||||
|
x.Plan == familiesPlan.PasswordManager.StripePlanId &&
|
||||||
|
x.Quantity == 1) &&
|
||||||
|
p.SubscriptionDetails.Items.Any(x =>
|
||||||
|
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
|
||||||
|
x.Quantity == 1)))
|
||||||
|
.Returns(new Invoice
|
||||||
|
{
|
||||||
|
TotalExcludingTax = 4000,
|
||||||
|
Tax = 800,
|
||||||
|
Total = 4800
|
||||||
|
});
|
||||||
|
|
||||||
|
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||||
|
|
||||||
|
Assert.Equal(8M, actual.TaxAmount);
|
||||||
|
Assert.Equal(48M, actual.TotalAmount);
|
||||||
|
Assert.Equal(40M, actual.TaxableBaseAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithoutAdditionalStorage(
|
||||||
|
SutProvider<StripePaymentService> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IAutomaticTaxFactory>()
|
||||||
|
.CreateAsync(Arg.Is<AutomaticTaxFactoryParameters>(p => p.PlanType == PlanType.FamiliesAnnually))
|
||||||
|
.Returns(new FakeAutomaticTaxStrategy(true));
|
||||||
|
|
||||||
|
var familiesPlan = new FamiliesPlan();
|
||||||
|
sutProvider.GetDependency<IPricingClient>()
|
||||||
|
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||||
|
.Returns(familiesPlan);
|
||||||
|
|
||||||
|
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||||
|
{
|
||||||
|
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||||
|
{
|
||||||
|
Plan = PlanType.FamiliesAnnually,
|
||||||
|
SponsoredPlan = PlanSponsorshipType.FamiliesForEnterprise,
|
||||||
|
AdditionalStorage = 0
|
||||||
|
},
|
||||||
|
TaxInformation = new TaxInformationRequestModel
|
||||||
|
{
|
||||||
|
Country = "FR",
|
||||||
|
PostalCode = "12345"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>()
|
||||||
|
.InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(p =>
|
||||||
|
p.Currency == "usd" &&
|
||||||
|
p.SubscriptionDetails.Items.Any(x =>
|
||||||
|
x.Plan == "2021-family-for-enterprise-annually" &&
|
||||||
|
x.Quantity == 1) &&
|
||||||
|
p.SubscriptionDetails.Items.Any(x =>
|
||||||
|
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
|
||||||
|
x.Quantity == 0)))
|
||||||
|
.Returns(new Invoice
|
||||||
|
{
|
||||||
|
TotalExcludingTax = 0,
|
||||||
|
Tax = 0,
|
||||||
|
Total = 0
|
||||||
|
});
|
||||||
|
|
||||||
|
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||||
|
|
||||||
|
Assert.Equal(0M, actual.TaxAmount);
|
||||||
|
Assert.Equal(0M, actual.TotalAmount);
|
||||||
|
Assert.Equal(0M, actual.TaxableBaseAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithAdditionalStorage(
|
||||||
|
SutProvider<StripePaymentService> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IAutomaticTaxFactory>()
|
||||||
|
.CreateAsync(Arg.Is<AutomaticTaxFactoryParameters>(p => p.PlanType == PlanType.FamiliesAnnually))
|
||||||
|
.Returns(new FakeAutomaticTaxStrategy(true));
|
||||||
|
|
||||||
|
var familiesPlan = new FamiliesPlan();
|
||||||
|
sutProvider.GetDependency<IPricingClient>()
|
||||||
|
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||||
|
.Returns(familiesPlan);
|
||||||
|
|
||||||
|
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||||
|
{
|
||||||
|
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||||
|
{
|
||||||
|
Plan = PlanType.FamiliesAnnually,
|
||||||
|
SponsoredPlan = PlanSponsorshipType.FamiliesForEnterprise,
|
||||||
|
AdditionalStorage = 1
|
||||||
|
},
|
||||||
|
TaxInformation = new TaxInformationRequestModel
|
||||||
|
{
|
||||||
|
Country = "FR",
|
||||||
|
PostalCode = "12345"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>()
|
||||||
|
.InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(p =>
|
||||||
|
p.Currency == "usd" &&
|
||||||
|
p.SubscriptionDetails.Items.Any(x =>
|
||||||
|
x.Plan == "2021-family-for-enterprise-annually" &&
|
||||||
|
x.Quantity == 1) &&
|
||||||
|
p.SubscriptionDetails.Items.Any(x =>
|
||||||
|
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
|
||||||
|
x.Quantity == 1)))
|
||||||
|
.Returns(new Invoice
|
||||||
|
{
|
||||||
|
TotalExcludingTax = 400,
|
||||||
|
Tax = 8,
|
||||||
|
Total = 408
|
||||||
|
});
|
||||||
|
|
||||||
|
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||||
|
|
||||||
|
Assert.Equal(0.08M, actual.TaxAmount);
|
||||||
|
Assert.Equal(4.08M, actual.TotalAmount);
|
||||||
|
Assert.Equal(4M, actual.TaxableBaseAmount);
|
||||||
|
}
|
||||||
|
}
|
@ -1228,7 +1228,8 @@ public class CipherServiceTests
|
|||||||
bool editPermission,
|
bool editPermission,
|
||||||
string? key = null,
|
string? key = null,
|
||||||
string? totp = null,
|
string? totp = null,
|
||||||
CipherLoginFido2CredentialData[]? passkeys = null
|
CipherLoginFido2CredentialData[]? passkeys = null,
|
||||||
|
CipherFieldData[]? fields = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var cipherDetails = new CipherDetails
|
var cipherDetails = new CipherDetails
|
||||||
@ -1241,12 +1242,13 @@ public class CipherServiceTests
|
|||||||
Key = key,
|
Key = key,
|
||||||
};
|
};
|
||||||
|
|
||||||
var newLoginData = new CipherLoginData { Username = "user", Password = newPassword, Totp = totp, Fido2Credentials = passkeys };
|
var newLoginData = new CipherLoginData { Username = "user", Password = newPassword, Totp = totp, Fido2Credentials = passkeys, Fields = fields };
|
||||||
cipherDetails.Data = JsonSerializer.Serialize(newLoginData);
|
cipherDetails.Data = JsonSerializer.Serialize(newLoginData);
|
||||||
|
|
||||||
var existingCipher = new Cipher
|
var existingCipher = new Cipher
|
||||||
{
|
{
|
||||||
Id = cipherDetails.Id,
|
Id = cipherDetails.Id,
|
||||||
|
Type = CipherType.Login,
|
||||||
Data = JsonSerializer.Serialize(
|
Data = JsonSerializer.Serialize(
|
||||||
new CipherLoginData
|
new CipherLoginData
|
||||||
{
|
{
|
||||||
@ -1442,6 +1444,56 @@ public class CipherServiceTests
|
|||||||
Assert.Equal(passkeys.Length, updatedLoginData.Fido2Credentials.Length);
|
Assert.Equal(passkeys.Length, updatedLoginData.Fido2Credentials.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_HiddenFieldsChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: false, editPermission: false, fields:
|
||||||
|
[
|
||||||
|
new CipherFieldData
|
||||||
|
{
|
||||||
|
Name = "FieldName",
|
||||||
|
Value = "FieldValue",
|
||||||
|
Type = FieldType.Hidden,
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||||
|
Assert.Empty(updatedLoginData.Fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_HiddenFieldsChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, fields:
|
||||||
|
[
|
||||||
|
new CipherFieldData
|
||||||
|
{
|
||||||
|
Name = "FieldName",
|
||||||
|
Value = "FieldValue",
|
||||||
|
Type = FieldType.Hidden,
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||||
|
Assert.Single(updatedLoginData.Fields.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task DeleteAsync_WithPersonalCipherOwner_DeletesCipher(
|
public async Task DeleteAsync_WithPersonalCipherOwner_DeletesCipher(
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using Bit.Identity.Models.Request.Accounts;
|
using Bit.Core;
|
||||||
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.IntegrationTestCommon.Factories;
|
using Bit.IntegrationTestCommon.Factories;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
@ -40,11 +42,20 @@ public class EventsApplicationFactory : WebApplicationFactoryBase<Startup>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<(string Token, string RefreshToken)> LoginWithNewAccount(string email = "integration-test@bitwarden.com", string masterPasswordHash = "master_password_hash")
|
public async Task<(string Token, string RefreshToken)> LoginWithNewAccount(string email = "integration-test@bitwarden.com", string masterPasswordHash = "master_password_hash")
|
||||||
{
|
{
|
||||||
await _identityApplicationFactory.RegisterAsync(new RegisterRequestModel
|
await _identityApplicationFactory.RegisterNewIdentityFactoryUserAsync(
|
||||||
{
|
new RegisterFinishRequestModel
|
||||||
Email = email,
|
{
|
||||||
MasterPasswordHash = masterPasswordHash,
|
Email = email,
|
||||||
});
|
MasterPasswordHash = masterPasswordHash,
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||||
|
UserAsymmetricKeys = new KeysRequestModel()
|
||||||
|
{
|
||||||
|
PublicKey = "public_key",
|
||||||
|
EncryptedPrivateKey = "private_key"
|
||||||
|
},
|
||||||
|
UserSymmetricKey = "sym_key",
|
||||||
|
});
|
||||||
|
|
||||||
return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash);
|
return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash);
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,8 @@ using Bit.Core.Entities;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Business.Tokenables;
|
using Bit.Core.Models.Business.Tokenables;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Identity.Models.Request.Accounts;
|
|
||||||
using Bit.IntegrationTestCommon.Factories;
|
using Bit.IntegrationTestCommon.Factories;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
@ -31,24 +29,6 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
_factory = factory;
|
_factory = factory;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task PostRegister_Success()
|
|
||||||
{
|
|
||||||
var context = await _factory.RegisterAsync(new RegisterRequestModel
|
|
||||||
{
|
|
||||||
Email = "test+register@email.com",
|
|
||||||
MasterPasswordHash = "master_password_hash"
|
|
||||||
});
|
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
|
||||||
|
|
||||||
var database = _factory.GetDatabaseContext();
|
|
||||||
var user = await database.Users
|
|
||||||
.SingleAsync(u => u.Email == "test+register@email.com");
|
|
||||||
|
|
||||||
Assert.NotNull(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData("invalidEmail")]
|
[BitAutoData("invalidEmail")]
|
||||||
[BitAutoData("")]
|
[BitAutoData("")]
|
||||||
@ -154,6 +134,7 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
|
// marketing emails can stay at top level
|
||||||
public async Task RegistrationWithEmailVerification_WithEmailVerificationToken_Succeeds([Required] string name, bool receiveMarketingEmails,
|
public async Task RegistrationWithEmailVerification_WithEmailVerificationToken_Succeeds([Required] string name, bool receiveMarketingEmails,
|
||||||
[StringLength(1000), Required] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, [Required] string userSymmetricKey,
|
[StringLength(1000), Required] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, [Required] string userSymmetricKey,
|
||||||
[Required] KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism)
|
[Required] KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism)
|
||||||
@ -161,16 +142,6 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
// Localize substitutions to this test.
|
// Localize substitutions to this test.
|
||||||
var localFactory = new IdentityApplicationFactory();
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
|
||||||
// First we must substitute the mail service in order to be able to get a valid email verification token
|
|
||||||
// for the complete registration step
|
|
||||||
string capturedEmailVerificationToken = null;
|
|
||||||
localFactory.SubstituteService<IMailService>(mailService =>
|
|
||||||
{
|
|
||||||
mailService.SendRegistrationVerificationEmailAsync(Arg.Any<string>(), Arg.Do<string>(t => capturedEmailVerificationToken = t))
|
|
||||||
.Returns(Task.CompletedTask);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// we must first call the send verification email endpoint to trigger the first part of the process
|
// we must first call the send verification email endpoint to trigger the first part of the process
|
||||||
var email = $"test+register+{name}@email.com";
|
var email = $"test+register+{name}@email.com";
|
||||||
var sendVerificationEmailReqModel = new RegisterSendVerificationEmailRequestModel
|
var sendVerificationEmailReqModel = new RegisterSendVerificationEmailRequestModel
|
||||||
@ -183,7 +154,7 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
var sendEmailVerificationResponseHttpContext = await localFactory.PostRegisterSendEmailVerificationAsync(sendVerificationEmailReqModel);
|
var sendEmailVerificationResponseHttpContext = await localFactory.PostRegisterSendEmailVerificationAsync(sendVerificationEmailReqModel);
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status204NoContent, sendEmailVerificationResponseHttpContext.Response.StatusCode);
|
Assert.Equal(StatusCodes.Status204NoContent, sendEmailVerificationResponseHttpContext.Response.StatusCode);
|
||||||
Assert.NotNull(capturedEmailVerificationToken);
|
Assert.NotNull(localFactory.RegistrationTokens[email]);
|
||||||
|
|
||||||
// Now we call the finish registration endpoint with the email verification token
|
// Now we call the finish registration endpoint with the email verification token
|
||||||
var registerFinishReqModel = new RegisterFinishRequestModel
|
var registerFinishReqModel = new RegisterFinishRequestModel
|
||||||
@ -191,7 +162,7 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
Email = email,
|
Email = email,
|
||||||
MasterPasswordHash = masterPasswordHash,
|
MasterPasswordHash = masterPasswordHash,
|
||||||
MasterPasswordHint = masterPasswordHint,
|
MasterPasswordHint = masterPasswordHint,
|
||||||
EmailVerificationToken = capturedEmailVerificationToken,
|
EmailVerificationToken = localFactory.RegistrationTokens[email],
|
||||||
Kdf = KdfType.PBKDF2_SHA256,
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||||
UserSymmetricKey = userSymmetricKey,
|
UserSymmetricKey = userSymmetricKey,
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
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;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
@ -13,7 +15,6 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Identity.Models.Request.Accounts;
|
|
||||||
using Bit.IntegrationTestCommon.Factories;
|
using Bit.IntegrationTestCommon.Factories;
|
||||||
using Bit.Test.Common.Helpers;
|
using Bit.Test.Common.Helpers;
|
||||||
using Duende.IdentityModel;
|
using Duende.IdentityModel;
|
||||||
@ -545,16 +546,15 @@ public class IdentityServerSsoTests
|
|||||||
{
|
{
|
||||||
var factory = new IdentityApplicationFactory();
|
var factory = new IdentityApplicationFactory();
|
||||||
|
|
||||||
|
|
||||||
var authorizationCode = new AuthorizationCode
|
var authorizationCode = new AuthorizationCode
|
||||||
{
|
{
|
||||||
ClientId = "web",
|
ClientId = "web",
|
||||||
CreationTime = DateTime.UtcNow,
|
CreationTime = DateTime.UtcNow,
|
||||||
Lifetime = (int)TimeSpan.FromMinutes(5).TotalSeconds,
|
Lifetime = (int)TimeSpan.FromMinutes(5).TotalSeconds,
|
||||||
RedirectUri = "https://localhost:8080/sso-connector.html",
|
RedirectUri = "https://localhost:8080/sso-connector.html",
|
||||||
RequestedScopes = new[] { "api", "offline_access" },
|
RequestedScopes = ["api", "offline_access"],
|
||||||
CodeChallenge = challenge.Sha256(),
|
CodeChallenge = challenge.Sha256(),
|
||||||
CodeChallengeMethod = "plain", //
|
CodeChallengeMethod = "plain",
|
||||||
Subject = null!, // Temporarily set it to null
|
Subject = null!, // Temporarily set it to null
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -564,16 +564,20 @@ public class IdentityServerSsoTests
|
|||||||
.Returns(authorizationCode);
|
.Returns(authorizationCode);
|
||||||
});
|
});
|
||||||
|
|
||||||
// This starts the server and finalizes services
|
var user = await factory.RegisterNewIdentityFactoryUserAsync(
|
||||||
var registerResponse = await factory.RegisterAsync(new RegisterRequestModel
|
new RegisterFinishRequestModel
|
||||||
{
|
{
|
||||||
Email = TestEmail,
|
Email = TestEmail,
|
||||||
MasterPasswordHash = "master_password_hash",
|
MasterPasswordHash = "masterPasswordHash",
|
||||||
});
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||||
var userRepository = factory.Services.GetRequiredService<IUserRepository>();
|
UserAsymmetricKeys = new KeysRequestModel()
|
||||||
var user = await userRepository.GetByEmailAsync(TestEmail);
|
{
|
||||||
Assert.NotNull(user);
|
PublicKey = "public_key",
|
||||||
|
EncryptedPrivateKey = "private_key"
|
||||||
|
},
|
||||||
|
UserSymmetricKey = "sym_key",
|
||||||
|
});
|
||||||
|
|
||||||
var organizationRepository = factory.Services.GetRequiredService<IOrganizationRepository>();
|
var organizationRepository = factory.Services.GetRequiredService<IOrganizationRepository>();
|
||||||
var organization = await organizationRepository.CreateAsync(new Organization
|
var organization = await organizationRepository.CreateAsync(new Organization
|
||||||
|
@ -3,11 +3,13 @@ using Bit.Core;
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Platform.Installations;
|
using Bit.Core.Platform.Installations;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Test.Auth.AutoFixture;
|
||||||
using Bit.Identity.IdentityServer;
|
using Bit.Identity.IdentityServer;
|
||||||
using Bit.Identity.Models.Request.Accounts;
|
|
||||||
using Bit.IntegrationTestCommon.Factories;
|
using Bit.IntegrationTestCommon.Factories;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using Bit.Test.Common.Helpers;
|
using Bit.Test.Common.Helpers;
|
||||||
@ -17,6 +19,7 @@ using Xunit;
|
|||||||
|
|
||||||
namespace Bit.Identity.IntegrationTest.Endpoints;
|
namespace Bit.Identity.IntegrationTest.Endpoints;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||||
{
|
{
|
||||||
private const int SecondsInMinute = 60;
|
private const int SecondsInMinute = 60;
|
||||||
@ -27,7 +30,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
public IdentityServerTests(IdentityApplicationFactory factory)
|
public IdentityServerTests(IdentityApplicationFactory factory)
|
||||||
{
|
{
|
||||||
_factory = factory;
|
_factory = factory;
|
||||||
ReinitializeDbForTests();
|
ReinitializeDbForTests(_factory);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -48,18 +51,14 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
AssertHelper.AssertEqualJson(endpointRoot, knownConfigurationRoot);
|
AssertHelper.AssertEqualJson(endpointRoot, knownConfigurationRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
|
||||||
public async Task TokenEndpoint_GrantTypePassword_Success(string deviceId)
|
public async Task TokenEndpoint_GrantTypePassword_Success(RegisterFinishRequestModel requestModel)
|
||||||
{
|
{
|
||||||
var username = "test+tokenpassword@email.com";
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
|
||||||
|
|
||||||
await _factory.RegisterAsync(new RegisterRequestModel
|
var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash,
|
||||||
{
|
context => context.SetAuthEmail(user.Email));
|
||||||
Email = username,
|
|
||||||
MasterPasswordHash = "master_password_hash"
|
|
||||||
});
|
|
||||||
|
|
||||||
var context = await PostLoginAsync(_factory.Server, username, deviceId, context => context.SetAuthEmail(username));
|
|
||||||
|
|
||||||
using var body = await AssertDefaultTokenBodyAsync(context);
|
using var body = await AssertDefaultTokenBodyAsync(context);
|
||||||
var root = body.RootElement;
|
var root = body.RootElement;
|
||||||
@ -73,18 +72,16 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
AssertUserDecryptionOptions(root);
|
AssertUserDecryptionOptions(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
|
||||||
public async Task TokenEndpoint_GrantTypePassword_NoAuthEmailHeader_Fails(string deviceId)
|
public async Task TokenEndpoint_GrantTypePassword_NoAuthEmailHeader_Fails(
|
||||||
|
RegisterFinishRequestModel requestModel)
|
||||||
{
|
{
|
||||||
var username = "test+noauthemailheader@email.com";
|
requestModel.Email = "test+noauthemailheader@email.com";
|
||||||
|
|
||||||
await _factory.RegisterAsync(new RegisterRequestModel
|
var localFactory = new IdentityApplicationFactory();
|
||||||
{
|
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
|
||||||
Email = username,
|
|
||||||
MasterPasswordHash = "master_password_hash",
|
|
||||||
});
|
|
||||||
|
|
||||||
var context = await PostLoginAsync(_factory.Server, username, deviceId, null);
|
var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash, null);
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||||
|
|
||||||
@ -96,18 +93,17 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String);
|
AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
|
||||||
public async Task TokenEndpoint_GrantTypePassword_InvalidBase64AuthEmailHeader_Fails(string deviceId)
|
public async Task TokenEndpoint_GrantTypePassword_InvalidBase64AuthEmailHeader_Fails(
|
||||||
|
RegisterFinishRequestModel requestModel)
|
||||||
{
|
{
|
||||||
var username = "test+badauthheader@email.com";
|
requestModel.Email = "test+badauthheader@email.com";
|
||||||
|
|
||||||
await _factory.RegisterAsync(new RegisterRequestModel
|
var localFactory = new IdentityApplicationFactory();
|
||||||
{
|
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
|
||||||
Email = username,
|
|
||||||
MasterPasswordHash = "master_password_hash",
|
|
||||||
});
|
|
||||||
|
|
||||||
var context = await PostLoginAsync(_factory.Server, username, deviceId, context => context.Request.Headers.Append("Auth-Email", "bad_value"));
|
var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash,
|
||||||
|
context => context.Request.Headers.Append("Auth-Email", "bad_value"));
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||||
|
|
||||||
@ -119,18 +115,17 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String);
|
AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
|
||||||
public async Task TokenEndpoint_GrantTypePassword_WrongAuthEmailHeader_Fails(string deviceId)
|
public async Task TokenEndpoint_GrantTypePassword_WrongAuthEmailHeader_Fails(
|
||||||
|
RegisterFinishRequestModel requestModel)
|
||||||
{
|
{
|
||||||
var username = "test+badauthheader@email.com";
|
requestModel.Email = "test+badauthheader@email.com";
|
||||||
|
|
||||||
await _factory.RegisterAsync(new RegisterRequestModel
|
var localFactory = new IdentityApplicationFactory();
|
||||||
{
|
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
|
||||||
Email = username,
|
|
||||||
MasterPasswordHash = "master_password_hash",
|
|
||||||
});
|
|
||||||
|
|
||||||
var context = await PostLoginAsync(_factory.Server, username, deviceId, context => context.SetAuthEmail("bad_value"));
|
var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash,
|
||||||
|
context => context.SetAuthEmail("bad_value"));
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||||
|
|
||||||
@ -142,215 +137,198 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String);
|
AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory, RegisterFinishRequestModelCustomize]
|
||||||
[BitAutoData(OrganizationUserType.Owner)]
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
[BitAutoData(OrganizationUserType.Admin)]
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
[BitAutoData(OrganizationUserType.User)]
|
[BitAutoData(OrganizationUserType.User)]
|
||||||
[BitAutoData(OrganizationUserType.Custom)]
|
[BitAutoData(OrganizationUserType.Custom)]
|
||||||
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyDisabled_WithEnforceSsoPolicyForAllUsersTrue_Success(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername)
|
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyDisabled_WithEnforceSsoPolicyForAllUsersTrue_Success(
|
||||||
|
OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername)
|
||||||
{
|
{
|
||||||
var username = $"{generatedUsername}@example.com";
|
requestModel.Email = $"{generatedUsername}@example.com";
|
||||||
|
|
||||||
var server = _factory.WithWebHostBuilder(builder =>
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
var server = localFactory.WithWebHostBuilder(builder =>
|
||||||
{
|
{
|
||||||
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "true");
|
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "true");
|
||||||
}).Server;
|
}).Server;
|
||||||
|
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
|
||||||
|
|
||||||
await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel
|
await CreateOrganizationWithSsoPolicyAsync(localFactory,
|
||||||
{
|
organizationId, user.Email, organizationUserType, ssoPolicyEnabled: false);
|
||||||
Email = username,
|
|
||||||
MasterPasswordHash = "master_password_hash"
|
|
||||||
}));
|
|
||||||
|
|
||||||
await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: false);
|
var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash,
|
||||||
|
context => context.SetAuthEmail(user.Email));
|
||||||
var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username));
|
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory, RegisterFinishRequestModelCustomize]
|
||||||
[BitAutoData(OrganizationUserType.Owner)]
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
[BitAutoData(OrganizationUserType.Admin)]
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
[BitAutoData(OrganizationUserType.User)]
|
[BitAutoData(OrganizationUserType.User)]
|
||||||
[BitAutoData(OrganizationUserType.Custom)]
|
[BitAutoData(OrganizationUserType.Custom)]
|
||||||
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyDisabled_WithEnforceSsoPolicyForAllUsersFalse_Success(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername)
|
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyDisabled_WithEnforceSsoPolicyForAllUsersFalse_Success(
|
||||||
|
OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername)
|
||||||
{
|
{
|
||||||
var username = $"{generatedUsername}@example.com";
|
requestModel.Email = $"{generatedUsername}@example.com";
|
||||||
|
|
||||||
var server = _factory.WithWebHostBuilder(builder =>
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
var server = localFactory.WithWebHostBuilder(builder =>
|
||||||
{
|
{
|
||||||
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "false");
|
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "false");
|
||||||
|
|
||||||
}).Server;
|
}).Server;
|
||||||
|
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
|
||||||
|
|
||||||
await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel
|
await CreateOrganizationWithSsoPolicyAsync(
|
||||||
{
|
localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: false);
|
||||||
Email = username,
|
|
||||||
MasterPasswordHash = "master_password_hash"
|
|
||||||
}));
|
|
||||||
|
|
||||||
await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: false);
|
var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash,
|
||||||
|
context => context.SetAuthEmail(user.Email));
|
||||||
var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username));
|
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory, RegisterFinishRequestModelCustomize]
|
||||||
[BitAutoData(OrganizationUserType.Owner)]
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
[BitAutoData(OrganizationUserType.Admin)]
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
[BitAutoData(OrganizationUserType.User)]
|
[BitAutoData(OrganizationUserType.User)]
|
||||||
[BitAutoData(OrganizationUserType.Custom)]
|
[BitAutoData(OrganizationUserType.Custom)]
|
||||||
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersTrue_Throw(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername)
|
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersTrue_Throw(
|
||||||
|
OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername)
|
||||||
{
|
{
|
||||||
var username = $"{generatedUsername}@example.com";
|
requestModel.Email = $"{generatedUsername}@example.com";
|
||||||
|
|
||||||
var server = _factory.WithWebHostBuilder(builder =>
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
var server = localFactory.WithWebHostBuilder(builder =>
|
||||||
{
|
{
|
||||||
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "true");
|
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "true");
|
||||||
}).Server;
|
}).Server;
|
||||||
|
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
|
||||||
|
|
||||||
await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel
|
await CreateOrganizationWithSsoPolicyAsync(localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: true);
|
||||||
{
|
|
||||||
Email = username,
|
|
||||||
MasterPasswordHash = "master_password_hash"
|
|
||||||
}));
|
|
||||||
|
|
||||||
await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: true);
|
var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash,
|
||||||
|
context => context.SetAuthEmail(user.Email));
|
||||||
var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username));
|
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||||
await AssertRequiredSsoAuthenticationResponseAsync(context);
|
await AssertRequiredSsoAuthenticationResponseAsync(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory, RegisterFinishRequestModelCustomize]
|
||||||
[BitAutoData(OrganizationUserType.Owner)]
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
[BitAutoData(OrganizationUserType.Admin)]
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
public async Task TokenEndpoint_GrantTypePassword_WithOwnerOrAdmin_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersFalse_Success(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername)
|
public async Task TokenEndpoint_GrantTypePassword_WithOwnerOrAdmin_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersFalse_Success(
|
||||||
|
OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername)
|
||||||
{
|
{
|
||||||
var username = $"{generatedUsername}@example.com";
|
requestModel.Email = $"{generatedUsername}@example.com";
|
||||||
|
|
||||||
var server = _factory.WithWebHostBuilder(builder =>
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
var server = localFactory.WithWebHostBuilder(builder =>
|
||||||
{
|
{
|
||||||
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "false");
|
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "false");
|
||||||
|
|
||||||
}).Server;
|
}).Server;
|
||||||
|
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
|
||||||
|
|
||||||
await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel
|
await CreateOrganizationWithSsoPolicyAsync(localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: true);
|
||||||
{
|
|
||||||
Email = username,
|
|
||||||
MasterPasswordHash = "master_password_hash"
|
|
||||||
}));
|
|
||||||
|
|
||||||
await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: true);
|
var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash,
|
||||||
|
context => context.SetAuthEmail(user.Email));
|
||||||
var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username));
|
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory, RegisterFinishRequestModelCustomize]
|
||||||
[BitAutoData(OrganizationUserType.User)]
|
[BitAutoData(OrganizationUserType.User)]
|
||||||
[BitAutoData(OrganizationUserType.Custom)]
|
[BitAutoData(OrganizationUserType.Custom)]
|
||||||
public async Task TokenEndpoint_GrantTypePassword_WithNonOwnerOrAdmin_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersFalse_Throws(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername)
|
public async Task TokenEndpoint_GrantTypePassword_WithNonOwnerOrAdmin_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersFalse_Throws(
|
||||||
|
OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername)
|
||||||
{
|
{
|
||||||
var username = $"{generatedUsername}@example.com";
|
requestModel.Email = $"{generatedUsername}@example.com";
|
||||||
|
|
||||||
var server = _factory.WithWebHostBuilder(builder =>
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
var server = localFactory.WithWebHostBuilder(builder =>
|
||||||
{
|
{
|
||||||
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "false");
|
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "false");
|
||||||
|
|
||||||
}).Server;
|
}).Server;
|
||||||
|
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
|
||||||
|
|
||||||
await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel
|
await CreateOrganizationWithSsoPolicyAsync(localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: true);
|
||||||
{
|
|
||||||
Email = username,
|
|
||||||
MasterPasswordHash = "master_password_hash"
|
|
||||||
}));
|
|
||||||
|
|
||||||
await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: true);
|
var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash,
|
||||||
|
context => context.SetAuthEmail(user.Email));
|
||||||
var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username));
|
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||||
await AssertRequiredSsoAuthenticationResponseAsync(context);
|
await AssertRequiredSsoAuthenticationResponseAsync(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
|
||||||
public async Task TokenEndpoint_GrantTypeRefreshToken_Success(string deviceId)
|
public async Task TokenEndpoint_GrantTypeRefreshToken_Success(RegisterFinishRequestModel requestModel)
|
||||||
{
|
{
|
||||||
var username = "test+tokenrefresh@email.com";
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
|
||||||
await _factory.RegisterAsync(new RegisterRequestModel
|
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
|
||||||
{
|
|
||||||
Email = username,
|
|
||||||
MasterPasswordHash = "master_password_hash",
|
|
||||||
});
|
|
||||||
|
|
||||||
var (_, refreshToken) = await _factory.TokenFromPasswordAsync(username, "master_password_hash", deviceId);
|
var (_, refreshToken) = await localFactory.TokenFromPasswordAsync(
|
||||||
|
requestModel.Email, requestModel.MasterPasswordHash);
|
||||||
|
|
||||||
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||||
{
|
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
{ "grant_type", "refresh_token" },
|
{
|
||||||
{ "client_id", "web" },
|
{ "grant_type", "refresh_token" },
|
||||||
{ "refresh_token", refreshToken },
|
{ "client_id", "web" },
|
||||||
}));
|
{ "refresh_token", refreshToken },
|
||||||
|
}));
|
||||||
|
|
||||||
using var body = await AssertDefaultTokenBodyAsync(context);
|
using var body = await AssertDefaultTokenBodyAsync(context);
|
||||||
AssertRefreshTokenExists(body.RootElement);
|
AssertRefreshTokenExists(body.RootElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
|
||||||
public async Task TokenEndpoint_GrantTypeClientCredentials_Success(string deviceId)
|
public async Task TokenEndpoint_GrantTypeClientCredentials_Success(RegisterFinishRequestModel model)
|
||||||
{
|
{
|
||||||
var username = "test+tokenclientcredentials@email.com";
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(model);
|
||||||
|
|
||||||
await _factory.RegisterAsync(new RegisterRequestModel
|
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||||
{
|
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
Email = username,
|
{
|
||||||
MasterPasswordHash = "master_password_hash",
|
{ "grant_type", "client_credentials" },
|
||||||
});
|
{ "client_id", $"user.{user.Id}" },
|
||||||
|
{ "client_secret", user.ApiKey },
|
||||||
var database = _factory.GetDatabaseContext();
|
{ "scope", "api" },
|
||||||
var user = await database.Users
|
{ "DeviceIdentifier", IdentityApplicationFactory.DefaultDeviceIdentifier },
|
||||||
.FirstAsync(u => u.Email == username);
|
{ "DeviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
||||||
|
{ "DeviceName", "firefox" },
|
||||||
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
})
|
||||||
{
|
);
|
||||||
{ "grant_type", "client_credentials" },
|
|
||||||
{ "client_id", $"user.{user.Id}" },
|
|
||||||
{ "client_secret", user.ApiKey },
|
|
||||||
{ "scope", "api" },
|
|
||||||
{ "DeviceIdentifier", deviceId },
|
|
||||||
{ "DeviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
|
||||||
{ "DeviceName", "firefox" },
|
|
||||||
}));
|
|
||||||
|
|
||||||
await AssertDefaultTokenBodyAsync(context, "api");
|
await AssertDefaultTokenBodyAsync(context, "api");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
|
||||||
public async Task TokenEndpoint_GrantTypeClientCredentials_AsLegacyUser_NotOnWebClient_Fails(string deviceId)
|
public async Task TokenEndpoint_GrantTypeClientCredentials_AsLegacyUser_NotOnWebClient_Fails(
|
||||||
|
RegisterFinishRequestModel model,
|
||||||
|
string deviceId)
|
||||||
{
|
{
|
||||||
var server = _factory.WithWebHostBuilder(builder =>
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
var server = localFactory.WithWebHostBuilder(builder =>
|
||||||
{
|
{
|
||||||
builder.UseSetting("globalSettings:launchDarkly:flagValues:block-legacy-users", "true");
|
builder.UseSetting("globalSettings:launchDarkly:flagValues:block-legacy-users", "true");
|
||||||
}).Server;
|
}).Server;
|
||||||
|
|
||||||
var username = "test+tokenclientcredentials@email.com";
|
model.Email = "test+tokenclientcredentials@email.com";
|
||||||
|
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(model);
|
||||||
|
|
||||||
|
// Modify user to be legacy user. We have to fetch the user again to put it in the ef-context
|
||||||
await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel
|
// so when we modify change tracking will save the changes.
|
||||||
{
|
var database = localFactory.GetDatabaseContext();
|
||||||
Email = username,
|
user = await database.Users
|
||||||
MasterPasswordHash = "master_password_hash"
|
.FirstAsync(u => u.Email == model.Email);
|
||||||
}));
|
user.Key = null;
|
||||||
|
|
||||||
|
|
||||||
var database = _factory.GetDatabaseContext();
|
|
||||||
var user = await database.Users
|
|
||||||
.FirstAsync(u => u.Email == username);
|
|
||||||
|
|
||||||
user.PrivateKey = "EncryptedPrivateKey";
|
|
||||||
await database.SaveChangesAsync();
|
await database.SaveChangesAsync();
|
||||||
|
|
||||||
var context = await server.PostAsync("/connect/token", new FormUrlEncodedContent(
|
var context = await server.PostAsync("/connect/token", new FormUrlEncodedContent(
|
||||||
@ -362,9 +340,9 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
{ "deviceIdentifier", deviceId },
|
{ "deviceIdentifier", deviceId },
|
||||||
{ "deviceName", "chrome" },
|
{ "deviceName", "chrome" },
|
||||||
{ "grant_type", "password" },
|
{ "grant_type", "password" },
|
||||||
{ "username", username },
|
{ "username", model.Email },
|
||||||
{ "password", "master_password_hash" },
|
{ "password", model.MasterPasswordHash },
|
||||||
}), context => context.SetAuthEmail(username));
|
}), context => context.SetAuthEmail(model.Email));
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||||
|
|
||||||
@ -535,23 +513,21 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
Assert.Equal("invalid_client", error);
|
Assert.Equal("invalid_client", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
|
||||||
public async Task TokenEndpoint_TooQuickInOneSecond_BlockRequest(string deviceId)
|
public async Task TokenEndpoint_TooQuickInOneSecond_BlockRequest(
|
||||||
|
RegisterFinishRequestModel requestModel)
|
||||||
{
|
{
|
||||||
const int AmountInOneSecondAllowed = 10;
|
const int AmountInOneSecondAllowed = 10;
|
||||||
|
|
||||||
// The rule we are testing is 10 requests in 1 second
|
// The rule we are testing is 10 requests in 1 second
|
||||||
var username = "test+ratelimiting@email.com";
|
requestModel.Email = "test+ratelimiting@email.com";
|
||||||
|
|
||||||
await _factory.RegisterAsync(new RegisterRequestModel
|
var localFactory = new IdentityApplicationFactory();
|
||||||
{
|
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
|
||||||
Email = username,
|
|
||||||
MasterPasswordHash = "master_password_hash",
|
|
||||||
});
|
|
||||||
|
|
||||||
var database = _factory.GetDatabaseContext();
|
var database = localFactory.GetDatabaseContext();
|
||||||
var user = await database.Users
|
user = await database.Users
|
||||||
.FirstAsync(u => u.Email == username);
|
.FirstAsync(u => u.Email == user.Email);
|
||||||
|
|
||||||
var tasks = new Task<HttpContext>[AmountInOneSecondAllowed + 1];
|
var tasks = new Task<HttpContext>[AmountInOneSecondAllowed + 1];
|
||||||
|
|
||||||
@ -573,36 +549,40 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
{ "scope", "api offline_access" },
|
{ "scope", "api offline_access" },
|
||||||
{ "client_id", "web" },
|
{ "client_id", "web" },
|
||||||
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
||||||
{ "deviceIdentifier", deviceId },
|
{ "deviceIdentifier", IdentityApplicationFactory.DefaultDeviceIdentifier },
|
||||||
{ "deviceName", "firefox" },
|
{ "deviceName", "firefox" },
|
||||||
{ "grant_type", "password" },
|
{ "grant_type", "password" },
|
||||||
{ "username", username },
|
{ "username", user.Email},
|
||||||
{ "password", "master_password_hash" },
|
{ "password", "master_password_hash" },
|
||||||
}), context => context.SetAuthEmail(username).SetIp("1.1.1.2"));
|
}), context => context.SetAuthEmail(user.Email).SetIp("1.1.1.2"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<HttpContext> PostLoginAsync(TestServer server, string username, string deviceId, Action<HttpContext> extraConfiguration)
|
private async Task<HttpContext> PostLoginAsync(
|
||||||
|
TestServer server, User user, string MasterPasswordHash, Action<HttpContext> extraConfiguration)
|
||||||
{
|
{
|
||||||
return await server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
return await server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
{ "scope", "api offline_access" },
|
{ "scope", "api offline_access" },
|
||||||
{ "client_id", "web" },
|
{ "client_id", "web" },
|
||||||
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
||||||
{ "deviceIdentifier", deviceId },
|
{ "deviceIdentifier", IdentityApplicationFactory.DefaultDeviceIdentifier },
|
||||||
{ "deviceName", "firefox" },
|
{ "deviceName", "firefox" },
|
||||||
{ "grant_type", "password" },
|
{ "grant_type", "password" },
|
||||||
{ "username", username },
|
{ "username", user.Email },
|
||||||
{ "password", "master_password_hash" },
|
{ "password", MasterPasswordHash },
|
||||||
}), extraConfiguration);
|
}), extraConfiguration);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CreateOrganizationWithSsoPolicyAsync(Guid organizationId, string username, OrganizationUserType organizationUserType, bool ssoPolicyEnabled)
|
private async Task CreateOrganizationWithSsoPolicyAsync(
|
||||||
|
IdentityApplicationFactory localFactory,
|
||||||
|
Guid organizationId,
|
||||||
|
string username, OrganizationUserType organizationUserType, bool ssoPolicyEnabled)
|
||||||
{
|
{
|
||||||
var userRepository = _factory.Services.GetService<IUserRepository>();
|
var userRepository = localFactory.Services.GetService<IUserRepository>();
|
||||||
var organizationRepository = _factory.Services.GetService<IOrganizationRepository>();
|
var organizationRepository = localFactory.Services.GetService<IOrganizationRepository>();
|
||||||
var organizationUserRepository = _factory.Services.GetService<IOrganizationUserRepository>();
|
var organizationUserRepository = localFactory.Services.GetService<IOrganizationUserRepository>();
|
||||||
var policyRepository = _factory.Services.GetService<IPolicyRepository>();
|
var policyRepository = localFactory.Services.GetService<IPolicyRepository>();
|
||||||
|
|
||||||
var organization = new Organization
|
var organization = new Organization
|
||||||
{
|
{
|
||||||
@ -617,7 +597,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
await organizationRepository.CreateAsync(organization);
|
await organizationRepository.CreateAsync(organization);
|
||||||
|
|
||||||
var user = await userRepository.GetByEmailAsync(username);
|
var user = await userRepository.GetByEmailAsync(username);
|
||||||
var organizationUser = new Bit.Core.Entities.OrganizationUser
|
var organizationUser = new OrganizationUser
|
||||||
{
|
{
|
||||||
OrganizationId = organization.Id,
|
OrganizationId = organization.Id,
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
@ -703,9 +683,9 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
(prop) => { Assert.Equal("Object", prop.Name); Assert.Equal("userDecryptionOptions", prop.Value.GetString()); });
|
(prop) => { Assert.Equal("Object", prop.Name); Assert.Equal("userDecryptionOptions", prop.Value.GetString()); });
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReinitializeDbForTests()
|
private void ReinitializeDbForTests(IdentityApplicationFactory factory)
|
||||||
{
|
{
|
||||||
var databaseContext = _factory.GetDatabaseContext();
|
var databaseContext = factory.GetDatabaseContext();
|
||||||
databaseContext.Policies.RemoveRange(databaseContext.Policies);
|
databaseContext.Policies.RemoveRange(databaseContext.Policies);
|
||||||
databaseContext.OrganizationUsers.RemoveRange(databaseContext.OrganizationUsers);
|
databaseContext.OrganizationUsers.RemoveRange(databaseContext.OrganizationUsers);
|
||||||
databaseContext.Organizations.RemoveRange(databaseContext.Organizations);
|
databaseContext.Organizations.RemoveRange(databaseContext.Organizations);
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
@ -11,7 +14,6 @@ using Bit.Core.Models.Data;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Identity.Models.Request.Accounts;
|
|
||||||
using Bit.IntegrationTestCommon.Factories;
|
using Bit.IntegrationTestCommon.Factories;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using Bit.Test.Common.Helpers;
|
using Bit.Test.Common.Helpers;
|
||||||
@ -19,6 +21,7 @@ using Duende.IdentityModel;
|
|||||||
using Duende.IdentityServer.Models;
|
using Duende.IdentityServer.Models;
|
||||||
using Duende.IdentityServer.Stores;
|
using Duende.IdentityServer.Stores;
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@ -61,19 +64,14 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
|||||||
public async Task TokenEndpoint_GrantTypePassword_UserTwoFactorRequired_TwoFactorProvided_Success()
|
public async Task TokenEndpoint_GrantTypePassword_UserTwoFactorRequired_TwoFactorProvided_Success()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
// we can't use the class factory here.
|
|
||||||
var factory = new IdentityApplicationFactory();
|
var factory = new IdentityApplicationFactory();
|
||||||
|
|
||||||
string emailToken = null;
|
// return specified email token from cache
|
||||||
factory.SubstituteService<IMailService>(mailService =>
|
var emailToken = "12345678";
|
||||||
|
factory.SubstituteService<IDistributedCache>(distCache =>
|
||||||
{
|
{
|
||||||
mailService.SendTwoFactorEmailAsync(
|
distCache.GetAsync(Arg.Is<string>(s => s.StartsWith("EmailToken_")))
|
||||||
Arg.Any<string>(),
|
.Returns(Task.FromResult(Encoding.UTF8.GetBytes(emailToken)));
|
||||||
Arg.Any<string>(),
|
|
||||||
Arg.Do<string>(t => emailToken = t),
|
|
||||||
Arg.Any<string>(),
|
|
||||||
Arg.Any<string>())
|
|
||||||
.Returns(Task.CompletedTask);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create Test User
|
// Create Test User
|
||||||
@ -102,10 +100,11 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
|||||||
public async Task TokenEndpoint_GrantTypePassword_InvalidTwoFactorToken_Fails()
|
public async Task TokenEndpoint_GrantTypePassword_InvalidTwoFactorToken_Fails()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
await CreateUserAsync(_factory, _testEmail, _userEmailTwoFactor);
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
await CreateUserAsync(localFactory, _testEmail, _userEmailTwoFactor);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var context = await _factory.ContextFromPasswordWithTwoFactorAsync(
|
var context = await localFactory.ContextFromPasswordWithTwoFactorAsync(
|
||||||
_testEmail, _testPassword, twoFactorProviderType: "Email");
|
_testEmail, _testPassword, twoFactorProviderType: "Email");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@ -124,16 +123,17 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
|||||||
public async Task TokenEndpoint_GrantTypePassword_OrgDuoTwoFactorRequired_NoTwoFactorProvided_Fails(string deviceId)
|
public async Task TokenEndpoint_GrantTypePassword_OrgDuoTwoFactorRequired_NoTwoFactorProvided_Fails(string deviceId)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var localFactory = new IdentityApplicationFactory();
|
||||||
var challenge = new string('c', 50);
|
var challenge = new string('c', 50);
|
||||||
var ssoConfigData = new SsoConfigurationData
|
var ssoConfigData = new SsoConfigurationData
|
||||||
{
|
{
|
||||||
MemberDecryptionType = MemberDecryptionType.MasterPassword,
|
MemberDecryptionType = MemberDecryptionType.MasterPassword,
|
||||||
};
|
};
|
||||||
await CreateSsoOrganizationAndUserAsync(
|
await CreateSsoOrganizationAndUserAsync(
|
||||||
_factory, ssoConfigData, challenge, _testEmail, orgTwoFactor: _organizationTwoFactor);
|
localFactory, ssoConfigData, challenge, _testEmail, orgTwoFactor: _organizationTwoFactor);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
var context = await localFactory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
{ "scope", "api offline_access" },
|
{ "scope", "api offline_access" },
|
||||||
{ "client_id", "web" },
|
{ "client_id", "web" },
|
||||||
@ -156,10 +156,11 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
|||||||
public async Task TokenEndpoint_GrantTypePassword_RememberTwoFactorType_InvalidTwoFactorToken_Fails()
|
public async Task TokenEndpoint_GrantTypePassword_RememberTwoFactorType_InvalidTwoFactorToken_Fails()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
await CreateUserAsync(_factory, _testEmail, _userEmailTwoFactor);
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
await CreateUserAsync(localFactory, _testEmail, _userEmailTwoFactor);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var context = await _factory.ContextFromPasswordWithTwoFactorAsync(
|
var context = await localFactory.ContextFromPasswordWithTwoFactorAsync(
|
||||||
_testEmail, _testPassword, twoFactorProviderType: "Remember");
|
_testEmail, _testPassword, twoFactorProviderType: "Remember");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@ -210,13 +211,14 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
|||||||
public async Task TokenEndpoint_GrantTypeClientCredential_IndvTwoFactorRequired_Success(string deviceId)
|
public async Task TokenEndpoint_GrantTypeClientCredential_IndvTwoFactorRequired_Success(string deviceId)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
await CreateUserAsync(_factory, _testEmail, _userEmailTwoFactor);
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
await CreateUserAsync(localFactory, _testEmail, _userEmailTwoFactor);
|
||||||
|
|
||||||
var database = _factory.GetDatabaseContext();
|
var database = localFactory.GetDatabaseContext();
|
||||||
var user = await database.Users.FirstAsync(u => u.Email == _testEmail);
|
var user = await database.Users.FirstAsync(u => u.Email == _testEmail);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
var context = await localFactory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
{ "grant_type", "client_credentials" },
|
{ "grant_type", "client_credentials" },
|
||||||
{ "client_id", $"user.{user.Id}" },
|
{ "client_id", $"user.{user.Id}" },
|
||||||
@ -275,16 +277,13 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var localFactory = new IdentityApplicationFactory();
|
var localFactory = new IdentityApplicationFactory();
|
||||||
string emailToken = null;
|
|
||||||
localFactory.SubstituteService<IMailService>(mailService =>
|
// return specified email token from cache
|
||||||
|
var emailToken = "12345678";
|
||||||
|
localFactory.SubstituteService<IDistributedCache>(distCache =>
|
||||||
{
|
{
|
||||||
mailService.SendTwoFactorEmailAsync(
|
distCache.GetAsync(Arg.Is<string>(s => s.StartsWith("EmailToken_")))
|
||||||
Arg.Any<string>(),
|
.Returns(Task.FromResult(Encoding.UTF8.GetBytes(emailToken)));
|
||||||
Arg.Any<string>(),
|
|
||||||
Arg.Do<string>(t => emailToken = t),
|
|
||||||
Arg.Any<string>(),
|
|
||||||
Arg.Any<string>())
|
|
||||||
.Returns(Task.CompletedTask);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create Test User
|
// Create Test User
|
||||||
@ -379,17 +378,24 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
|||||||
string userTwoFactor = null)
|
string userTwoFactor = null)
|
||||||
{
|
{
|
||||||
// Create Test User
|
// Create Test User
|
||||||
await factory.RegisterAsync(new RegisterRequestModel
|
var user = await factory.RegisterNewIdentityFactoryUserAsync(
|
||||||
{
|
new RegisterFinishRequestModel
|
||||||
Email = testEmail,
|
{
|
||||||
MasterPasswordHash = _testPassword,
|
Email = testEmail,
|
||||||
});
|
MasterPasswordHash = _testPassword,
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
var userRepository = factory.Services.GetRequiredService<IUserRepository>();
|
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||||
var user = await userRepository.GetByEmailAsync(testEmail);
|
UserAsymmetricKeys = new KeysRequestModel()
|
||||||
|
{
|
||||||
|
PublicKey = "public_key",
|
||||||
|
EncryptedPrivateKey = "private_key"
|
||||||
|
},
|
||||||
|
UserSymmetricKey = "sym_key",
|
||||||
|
});
|
||||||
Assert.NotNull(user);
|
Assert.NotNull(user);
|
||||||
|
|
||||||
var userService = factory.GetService<IUserService>();
|
var userService = factory.GetService<IUserService>();
|
||||||
|
var userRepository = factory.Services.GetRequiredService<IUserRepository>();
|
||||||
if (userTwoFactor != null)
|
if (userTwoFactor != null)
|
||||||
{
|
{
|
||||||
user.TwoFactorProviders = userTwoFactor;
|
user.TwoFactorProviders = userTwoFactor;
|
||||||
@ -426,16 +432,20 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
|||||||
.Returns(authorizationCode);
|
.Returns(authorizationCode);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create Test User
|
var user = await factory.RegisterNewIdentityFactoryUserAsync(
|
||||||
var registerResponse = await factory.RegisterAsync(new RegisterRequestModel
|
new RegisterFinishRequestModel
|
||||||
{
|
{
|
||||||
Email = testEmail,
|
Email = testEmail,
|
||||||
MasterPasswordHash = _testPassword,
|
MasterPasswordHash = _testPassword,
|
||||||
});
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||||
var userRepository = factory.Services.GetRequiredService<IUserRepository>();
|
UserAsymmetricKeys = new KeysRequestModel()
|
||||||
var user = await userRepository.GetByEmailAsync(testEmail);
|
{
|
||||||
Assert.NotNull(user);
|
PublicKey = "public_key",
|
||||||
|
EncryptedPrivateKey = "private_key"
|
||||||
|
},
|
||||||
|
UserSymmetricKey = "sym_key",
|
||||||
|
});
|
||||||
|
|
||||||
var userService = factory.GetService<IUserService>();
|
var userService = factory.GetService<IUserService>();
|
||||||
if (userTwoFactor != null)
|
if (userTwoFactor != null)
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\Identity\Identity.csproj" />
|
<ProjectReference Include="..\..\src\Identity\Identity.csproj" />
|
||||||
<ProjectReference Include="..\Common\Common.csproj" />
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
|
<ProjectReference Include="..\Core.Test\Core.Test.csproj" />
|
||||||
<ProjectReference Include="..\IntegrationTestCommon\IntegrationTestCommon.csproj" />
|
<ProjectReference Include="..\IntegrationTestCommon\IntegrationTestCommon.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Identity.Models.Request.Accounts;
|
|
||||||
using Bit.IntegrationTestCommon.Factories;
|
using Bit.IntegrationTestCommon.Factories;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using Bit.Test.Common.Helpers;
|
using Bit.Test.Common.Helpers;
|
||||||
@ -19,28 +19,16 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
|||||||
private const string DefaultPassword = "master_password_hash";
|
private const string DefaultPassword = "master_password_hash";
|
||||||
private const string DefaultUsername = "test@email.qa";
|
private const string DefaultUsername = "test@email.qa";
|
||||||
private const string DefaultDeviceIdentifier = "test_identifier";
|
private const string DefaultDeviceIdentifier = "test_identifier";
|
||||||
private readonly IdentityApplicationFactory _factory;
|
|
||||||
private readonly UserManager<User> _userManager;
|
|
||||||
private readonly IAuthRequestRepository _authRequestRepository;
|
|
||||||
private readonly IDeviceService _deviceService;
|
|
||||||
|
|
||||||
public ResourceOwnerPasswordValidatorTests(IdentityApplicationFactory factory)
|
|
||||||
{
|
|
||||||
_factory = factory;
|
|
||||||
|
|
||||||
_userManager = _factory.GetService<UserManager<User>>();
|
|
||||||
_authRequestRepository = _factory.GetService<IAuthRequestRepository>();
|
|
||||||
_deviceService = _factory.GetService<IDeviceService>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateAsync_Success()
|
public async Task ValidateAsync_Success()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
await EnsureUserCreatedAsync();
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
await EnsureUserCreatedAsync(localFactory);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var context = await _factory.Server.PostAsync("/connect/token",
|
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||||
GetFormUrlEncodedContent(),
|
GetFormUrlEncodedContent(),
|
||||||
context => context.SetAuthEmail(DefaultUsername));
|
context => context.SetAuthEmail(DefaultUsername));
|
||||||
|
|
||||||
@ -56,10 +44,11 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
|||||||
public async Task ValidateAsync_AuthEmailHeaderInvalid_InvalidGrantResponse()
|
public async Task ValidateAsync_AuthEmailHeaderInvalid_InvalidGrantResponse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
await EnsureUserCreatedAsync();
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
await EnsureUserCreatedAsync(localFactory);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var context = await _factory.Server.PostAsync(
|
var context = await localFactory.Server.PostAsync(
|
||||||
"/connect/token",
|
"/connect/token",
|
||||||
GetFormUrlEncodedContent()
|
GetFormUrlEncodedContent()
|
||||||
);
|
);
|
||||||
@ -75,8 +64,10 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task ValidateAsync_UserNull_Failure(string username)
|
public async Task ValidateAsync_UserNull_Failure(string username)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
|
var localFactory = new IdentityApplicationFactory();
|
||||||
// Act
|
// Act
|
||||||
var context = await _factory.Server.PostAsync("/connect/token",
|
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||||
GetFormUrlEncodedContent(username: username),
|
GetFormUrlEncodedContent(username: username),
|
||||||
context => context.SetAuthEmail(username));
|
context => context.SetAuthEmail(username));
|
||||||
|
|
||||||
@ -105,13 +96,16 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
|||||||
public async Task ValidateAsync_BadPassword_Failure(string badPassword)
|
public async Task ValidateAsync_BadPassword_Failure(string badPassword)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
await EnsureUserCreatedAsync();
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
await EnsureUserCreatedAsync(localFactory);
|
||||||
|
|
||||||
|
var userManager = localFactory.GetService<UserManager<User>>();
|
||||||
|
|
||||||
// Verify the User is not null to ensure the failure is due to bad password
|
// Verify the User is not null to ensure the failure is due to bad password
|
||||||
Assert.NotNull(await _userManager.FindByEmailAsync(DefaultUsername));
|
Assert.NotNull(await userManager.FindByEmailAsync(DefaultUsername));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var context = await _factory.Server.PostAsync("/connect/token",
|
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||||
GetFormUrlEncodedContent(password: badPassword),
|
GetFormUrlEncodedContent(password: badPassword),
|
||||||
context => context.SetAuthEmail(DefaultUsername));
|
context => context.SetAuthEmail(DefaultUsername));
|
||||||
|
|
||||||
@ -128,9 +122,12 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
|||||||
public async Task ValidateAsync_ValidateContextAsync_AuthRequest_NotNull_AgeLessThanOneHour_Success()
|
public async Task ValidateAsync_ValidateContextAsync_AuthRequest_NotNull_AgeLessThanOneHour_Success()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
|
||||||
// Ensure User
|
// Ensure User
|
||||||
await EnsureUserCreatedAsync();
|
await EnsureUserCreatedAsync(localFactory);
|
||||||
var user = await _userManager.FindByEmailAsync(DefaultUsername);
|
var userManager = localFactory.GetService<UserManager<User>>();
|
||||||
|
var user = await userManager.FindByEmailAsync(DefaultUsername);
|
||||||
Assert.NotNull(user);
|
Assert.NotNull(user);
|
||||||
|
|
||||||
// Connect Request to User and set CreationDate
|
// Connect Request to User and set CreationDate
|
||||||
@ -139,13 +136,14 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
|||||||
AuthRequestType.AuthenticateAndUnlock,
|
AuthRequestType.AuthenticateAndUnlock,
|
||||||
DateTime.UtcNow.AddMinutes(-30)
|
DateTime.UtcNow.AddMinutes(-30)
|
||||||
);
|
);
|
||||||
await _authRequestRepository.CreateAsync(authRequest);
|
var authRequestRepository = localFactory.GetService<IAuthRequestRepository>();
|
||||||
|
await authRequestRepository.CreateAsync(authRequest);
|
||||||
|
|
||||||
var expectedAuthRequest = await _authRequestRepository.GetManyByUserIdAsync(user.Id);
|
var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user.Id);
|
||||||
Assert.NotEmpty(expectedAuthRequest);
|
Assert.NotEmpty(expectedAuthRequest);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var context = await _factory.Server.PostAsync("/connect/token",
|
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||||
new FormUrlEncodedContent(new Dictionary<string, string>
|
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
{ "scope", "api offline_access" },
|
{ "scope", "api offline_access" },
|
||||||
@ -171,9 +169,12 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
|||||||
public async Task ValidateAsync_ValidateContextAsync_AuthRequest_NotNull_AgeGreaterThanOneHour_Failure()
|
public async Task ValidateAsync_ValidateContextAsync_AuthRequest_NotNull_AgeGreaterThanOneHour_Failure()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
var localFactory = new IdentityApplicationFactory();
|
||||||
// Ensure User
|
// Ensure User
|
||||||
await EnsureUserCreatedAsync(_factory);
|
await EnsureUserCreatedAsync(localFactory);
|
||||||
var user = await _userManager.FindByEmailAsync(DefaultUsername);
|
var userManager = localFactory.GetService<UserManager<User>>();
|
||||||
|
|
||||||
|
var user = await userManager.FindByEmailAsync(DefaultUsername);
|
||||||
Assert.NotNull(user);
|
Assert.NotNull(user);
|
||||||
|
|
||||||
// Create AuthRequest
|
// Create AuthRequest
|
||||||
@ -184,7 +185,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var context = await _factory.Server.PostAsync("/connect/token",
|
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||||
new FormUrlEncodedContent(new Dictionary<string, string>
|
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
{ "scope", "api offline_access" },
|
{ "scope", "api offline_access" },
|
||||||
@ -214,19 +215,23 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
|||||||
Assert.Equal("Username or password is incorrect. Try again.", errorMessage);
|
Assert.Equal("Username or password is incorrect. Try again.", errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnsureUserCreatedAsync(IdentityApplicationFactory factory = null)
|
private async Task EnsureUserCreatedAsync(IdentityApplicationFactory factory)
|
||||||
{
|
{
|
||||||
factory ??= _factory;
|
// Register user
|
||||||
// No need to create more users than we need
|
await factory.RegisterNewIdentityFactoryUserAsync(
|
||||||
if (await _userManager.FindByEmailAsync(DefaultUsername) == null)
|
new RegisterFinishRequestModel
|
||||||
{
|
|
||||||
// Register user
|
|
||||||
await factory.RegisterAsync(new RegisterRequestModel
|
|
||||||
{
|
{
|
||||||
Email = DefaultUsername,
|
Email = DefaultUsername,
|
||||||
MasterPasswordHash = DefaultPassword
|
MasterPasswordHash = DefaultPassword,
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||||
|
UserAsymmetricKeys = new KeysRequestModel()
|
||||||
|
{
|
||||||
|
PublicKey = "public_key",
|
||||||
|
EncryptedPrivateKey = "private_key"
|
||||||
|
},
|
||||||
|
UserSymmetricKey = "sym_key",
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private FormUrlEncodedContent GetFormUrlEncodedContent(
|
private FormUrlEncodedContent GetFormUrlEncodedContent(
|
||||||
|
@ -144,50 +144,6 @@ public class AccountsControllerTests : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task PostRegister_ShouldRegisterUser()
|
|
||||||
{
|
|
||||||
var passwordHash = "abcdef";
|
|
||||||
var token = "123456";
|
|
||||||
var userGuid = new Guid();
|
|
||||||
_registerUserCommand.RegisterUserViaOrganizationInviteToken(Arg.Any<User>(), passwordHash, token, userGuid)
|
|
||||||
.Returns(Task.FromResult(IdentityResult.Success));
|
|
||||||
var request = new RegisterRequestModel
|
|
||||||
{
|
|
||||||
Name = "Example User",
|
|
||||||
Email = "user@example.com",
|
|
||||||
MasterPasswordHash = passwordHash,
|
|
||||||
MasterPasswordHint = "example",
|
|
||||||
Token = token,
|
|
||||||
OrganizationUserId = userGuid
|
|
||||||
};
|
|
||||||
|
|
||||||
await _sut.PostRegister(request);
|
|
||||||
|
|
||||||
await _registerUserCommand.Received(1).RegisterUserViaOrganizationInviteToken(Arg.Any<User>(), passwordHash, token, userGuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task PostRegister_WhenUserServiceFails_ShouldThrowBadRequestException()
|
|
||||||
{
|
|
||||||
var passwordHash = "abcdef";
|
|
||||||
var token = "123456";
|
|
||||||
var userGuid = new Guid();
|
|
||||||
_registerUserCommand.RegisterUserViaOrganizationInviteToken(Arg.Any<User>(), passwordHash, token, userGuid)
|
|
||||||
.Returns(Task.FromResult(IdentityResult.Failed()));
|
|
||||||
var request = new RegisterRequestModel
|
|
||||||
{
|
|
||||||
Name = "Example User",
|
|
||||||
Email = "user@example.com",
|
|
||||||
MasterPasswordHash = passwordHash,
|
|
||||||
MasterPasswordHint = "example",
|
|
||||||
Token = token,
|
|
||||||
OrganizationUserId = userGuid
|
|
||||||
};
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostRegister(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task PostRegisterSendEmailVerification_WhenTokenReturnedFromCommand_Returns200WithToken(string email, string name, bool receiveMarketingEmails)
|
public async Task PostRegisterSendEmailVerification_WhenTokenReturnedFromCommand_Returns200WithToken(string email, string name, bool receiveMarketingEmails)
|
||||||
|
@ -1,23 +1,51 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Identity;
|
using Bit.Identity;
|
||||||
using Bit.Identity.Models.Request.Accounts;
|
|
||||||
using Bit.Test.Common.Helpers;
|
using Bit.Test.Common.Helpers;
|
||||||
using HandlebarsDotNet;
|
using HandlebarsDotNet;
|
||||||
|
using LinqToDB;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.IntegrationTestCommon.Factories;
|
namespace Bit.IntegrationTestCommon.Factories;
|
||||||
|
|
||||||
public class IdentityApplicationFactory : WebApplicationFactoryBase<Startup>
|
public class IdentityApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||||
{
|
{
|
||||||
public const string DefaultDeviceIdentifier = "92b9d953-b9b6-4eaf-9d3e-11d57144dfeb";
|
public const string DefaultDeviceIdentifier = "92b9d953-b9b6-4eaf-9d3e-11d57144dfeb";
|
||||||
|
public const string DefaultUserEmail = "DefaultEmail@bitwarden.com";
|
||||||
|
public const string DefaultUserPasswordHash = "default_password_hash";
|
||||||
|
|
||||||
public async Task<HttpContext> RegisterAsync(RegisterRequestModel model)
|
/// <summary>
|
||||||
|
/// A dictionary to store registration tokens for email verification. We cannot substitute the IMailService more than once, so
|
||||||
|
/// we capture the email tokens for new user registration in the constructor. The email must be unique otherwise an error will be thrown.
|
||||||
|
/// </summary>
|
||||||
|
public ConcurrentDictionary<string, string> RegistrationTokens { get; private set; } = new ConcurrentDictionary<string, string>();
|
||||||
|
|
||||||
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||||
{
|
{
|
||||||
return await Server.PostAsync("/accounts/register", JsonContent.Create(model));
|
// This allows us to use the official registration flow
|
||||||
|
SubstituteService<IMailService>(service =>
|
||||||
|
{
|
||||||
|
service.SendRegistrationVerificationEmailAsync(Arg.Any<string>(), Arg.Any<string>())
|
||||||
|
.ReturnsForAnyArgs(Task.CompletedTask)
|
||||||
|
.AndDoes(call =>
|
||||||
|
{
|
||||||
|
if (!RegistrationTokens.TryAdd(call.ArgAt<string>(0), call.ArgAt<string>(1)))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("This email was already registered for new user registration.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
base.ConfigureWebHost(builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HttpContext> PostRegisterSendEmailVerificationAsync(RegisterSendVerificationEmailRequestModel model)
|
public async Task<HttpContext> PostRegisterSendEmailVerificationAsync(RegisterSendVerificationEmailRequestModel model)
|
||||||
@ -155,4 +183,42 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase<Startup>
|
|||||||
}));
|
}));
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a new user to the Identity Application Factory based on the RegisterFinishRequestModel
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestModel">RegisterFinishRequestModel needed to seed data to the test user</param>
|
||||||
|
/// <param name="marketingEmails">optional parameter that is tracked during the inital steps of registration.</param>
|
||||||
|
/// <returns>returns the newly created user</returns>
|
||||||
|
public async Task<User> RegisterNewIdentityFactoryUserAsync(
|
||||||
|
RegisterFinishRequestModel requestModel,
|
||||||
|
bool marketingEmails = true)
|
||||||
|
{
|
||||||
|
var sendVerificationEmailReqModel = new RegisterSendVerificationEmailRequestModel
|
||||||
|
{
|
||||||
|
Email = requestModel.Email,
|
||||||
|
Name = "name",
|
||||||
|
ReceiveMarketingEmails = marketingEmails
|
||||||
|
};
|
||||||
|
|
||||||
|
var sendEmailVerificationResponseHttpContext = await PostRegisterSendEmailVerificationAsync(sendVerificationEmailReqModel);
|
||||||
|
|
||||||
|
Assert.Equal(StatusCodes.Status204NoContent, sendEmailVerificationResponseHttpContext.Response.StatusCode);
|
||||||
|
Assert.NotNull(RegistrationTokens[requestModel.Email]);
|
||||||
|
|
||||||
|
// Now we call the finish registration endpoint with the email verification token
|
||||||
|
requestModel.EmailVerificationToken = RegistrationTokens[requestModel.Email];
|
||||||
|
|
||||||
|
var postRegisterFinishHttpContext = await PostRegisterFinishAsync(requestModel);
|
||||||
|
|
||||||
|
Assert.Equal(StatusCodes.Status200OK, postRegisterFinishHttpContext.Response.StatusCode);
|
||||||
|
|
||||||
|
var database = GetDatabaseContext();
|
||||||
|
var user = await database.Users
|
||||||
|
.SingleAsync(u => u.Email == requestModel.Email);
|
||||||
|
|
||||||
|
Assert.NotNull(user);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,127 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Organization_Update]
|
||||||
|
@Id UNIQUEIDENTIFIER,
|
||||||
|
@Identifier NVARCHAR(50),
|
||||||
|
@Name NVARCHAR(50),
|
||||||
|
@BusinessName NVARCHAR(50),
|
||||||
|
@BusinessAddress1 NVARCHAR(50),
|
||||||
|
@BusinessAddress2 NVARCHAR(50),
|
||||||
|
@BusinessAddress3 NVARCHAR(50),
|
||||||
|
@BusinessCountry VARCHAR(2),
|
||||||
|
@BusinessTaxNumber NVARCHAR(30),
|
||||||
|
@BillingEmail NVARCHAR(256),
|
||||||
|
@Plan NVARCHAR(50),
|
||||||
|
@PlanType TINYINT,
|
||||||
|
@Seats INT,
|
||||||
|
@MaxCollections SMALLINT,
|
||||||
|
@UsePolicies BIT,
|
||||||
|
@UseSso BIT,
|
||||||
|
@UseGroups BIT,
|
||||||
|
@UseDirectory BIT,
|
||||||
|
@UseEvents BIT,
|
||||||
|
@UseTotp BIT,
|
||||||
|
@Use2fa BIT,
|
||||||
|
@UseApi BIT,
|
||||||
|
@UseResetPassword BIT,
|
||||||
|
@SelfHost BIT,
|
||||||
|
@UsersGetPremium BIT,
|
||||||
|
@Storage BIGINT,
|
||||||
|
@MaxStorageGb SMALLINT,
|
||||||
|
@Gateway TINYINT,
|
||||||
|
@GatewayCustomerId VARCHAR(50),
|
||||||
|
@GatewaySubscriptionId VARCHAR(50),
|
||||||
|
@ReferenceData VARCHAR(MAX),
|
||||||
|
@Enabled BIT,
|
||||||
|
@LicenseKey VARCHAR(100),
|
||||||
|
@PublicKey VARCHAR(MAX),
|
||||||
|
@PrivateKey VARCHAR(MAX),
|
||||||
|
@TwoFactorProviders NVARCHAR(MAX),
|
||||||
|
@ExpirationDate DATETIME2(7),
|
||||||
|
@CreationDate DATETIME2(7),
|
||||||
|
@RevisionDate DATETIME2(7),
|
||||||
|
@OwnersNotifiedOfAutoscaling DATETIME2(7),
|
||||||
|
@MaxAutoscaleSeats INT,
|
||||||
|
@UseKeyConnector BIT = 0,
|
||||||
|
@UseScim BIT = 0,
|
||||||
|
@UseCustomPermissions BIT = 0,
|
||||||
|
@UseSecretsManager BIT = 0,
|
||||||
|
@Status TINYINT = 0,
|
||||||
|
@UsePasswordManager BIT = 1,
|
||||||
|
@SmSeats INT = null,
|
||||||
|
@SmServiceAccounts INT = null,
|
||||||
|
@MaxAutoscaleSmSeats INT = null,
|
||||||
|
@MaxAutoscaleSmServiceAccounts INT = null,
|
||||||
|
@SecretsManagerBeta BIT = 0,
|
||||||
|
@LimitCollectionCreation BIT = null,
|
||||||
|
@LimitCollectionDeletion BIT = null,
|
||||||
|
@AllowAdminAccessToAllCollectionItems BIT = 0,
|
||||||
|
@UseRiskInsights BIT = 0,
|
||||||
|
@LimitItemDeletion BIT = 0,
|
||||||
|
@UseAdminSponsoredFamilies BIT = 0
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
UPDATE
|
||||||
|
[dbo].[Organization]
|
||||||
|
SET
|
||||||
|
[Identifier] = @Identifier,
|
||||||
|
[Name] = @Name,
|
||||||
|
[BusinessName] = @BusinessName,
|
||||||
|
[BusinessAddress1] = @BusinessAddress1,
|
||||||
|
[BusinessAddress2] = @BusinessAddress2,
|
||||||
|
[BusinessAddress3] = @BusinessAddress3,
|
||||||
|
[BusinessCountry] = @BusinessCountry,
|
||||||
|
[BusinessTaxNumber] = @BusinessTaxNumber,
|
||||||
|
[BillingEmail] = @BillingEmail,
|
||||||
|
[Plan] = @Plan,
|
||||||
|
[PlanType] = @PlanType,
|
||||||
|
[Seats] = @Seats,
|
||||||
|
[MaxCollections] = @MaxCollections,
|
||||||
|
[UsePolicies] = @UsePolicies,
|
||||||
|
[UseSso] = @UseSso,
|
||||||
|
[UseGroups] = @UseGroups,
|
||||||
|
[UseDirectory] = @UseDirectory,
|
||||||
|
[UseEvents] = @UseEvents,
|
||||||
|
[UseTotp] = @UseTotp,
|
||||||
|
[Use2fa] = @Use2fa,
|
||||||
|
[UseApi] = @UseApi,
|
||||||
|
[UseResetPassword] = @UseResetPassword,
|
||||||
|
[SelfHost] = @SelfHost,
|
||||||
|
[UsersGetPremium] = @UsersGetPremium,
|
||||||
|
[Storage] = @Storage,
|
||||||
|
[MaxStorageGb] = @MaxStorageGb,
|
||||||
|
[Gateway] = @Gateway,
|
||||||
|
[GatewayCustomerId] = @GatewayCustomerId,
|
||||||
|
[GatewaySubscriptionId] = @GatewaySubscriptionId,
|
||||||
|
[ReferenceData] = @ReferenceData,
|
||||||
|
[Enabled] = @Enabled,
|
||||||
|
[LicenseKey] = @LicenseKey,
|
||||||
|
[PublicKey] = @PublicKey,
|
||||||
|
[PrivateKey] = @PrivateKey,
|
||||||
|
[TwoFactorProviders] = @TwoFactorProviders,
|
||||||
|
[ExpirationDate] = @ExpirationDate,
|
||||||
|
[CreationDate] = @CreationDate,
|
||||||
|
[RevisionDate] = @RevisionDate,
|
||||||
|
[OwnersNotifiedOfAutoscaling] = @OwnersNotifiedOfAutoscaling,
|
||||||
|
[MaxAutoscaleSeats] = @MaxAutoscaleSeats,
|
||||||
|
[UseKeyConnector] = @UseKeyConnector,
|
||||||
|
[UseScim] = @UseScim,
|
||||||
|
[UseCustomPermissions] = @UseCustomPermissions,
|
||||||
|
[UseSecretsManager] = @UseSecretsManager,
|
||||||
|
[Status] = @Status,
|
||||||
|
[UsePasswordManager] = @UsePasswordManager,
|
||||||
|
[SmSeats] = @SmSeats,
|
||||||
|
[SmServiceAccounts] = @SmServiceAccounts,
|
||||||
|
[MaxAutoscaleSmSeats] = @MaxAutoscaleSmSeats,
|
||||||
|
[MaxAutoscaleSmServiceAccounts] = @MaxAutoscaleSmServiceAccounts,
|
||||||
|
[SecretsManagerBeta] = @SecretsManagerBeta,
|
||||||
|
[LimitCollectionCreation] = @LimitCollectionCreation,
|
||||||
|
[LimitCollectionDeletion] = @LimitCollectionDeletion,
|
||||||
|
[AllowAdminAccessToAllCollectionItems] = @AllowAdminAccessToAllCollectionItems,
|
||||||
|
[UseRiskInsights] = @UseRiskInsights,
|
||||||
|
[LimitItemDeletion] = @LimitItemDeletion,
|
||||||
|
[UseAdminSponsoredFamilies] = @UseAdminSponsoredFamilies
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
END
|
||||||
|
GO
|
@ -0,0 +1,571 @@
|
|||||||
|
IF EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[Organization]') AND name = 'UseAdminSponsoredFamilies')
|
||||||
|
BEGIN
|
||||||
|
-- First drop the default constraint
|
||||||
|
DECLARE @ConstraintName nvarchar(200)
|
||||||
|
SELECT @ConstraintName = name FROM sys.default_constraints
|
||||||
|
WHERE parent_object_id = OBJECT_ID(N'[dbo].[Organization]')
|
||||||
|
AND parent_column_id = (SELECT column_id FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[Organization]') AND name = 'UseAdminSponsoredFamilies')
|
||||||
|
|
||||||
|
IF @ConstraintName IS NOT NULL
|
||||||
|
EXEC('ALTER TABLE [dbo].[Organization] DROP CONSTRAINT ' + @ConstraintName)
|
||||||
|
|
||||||
|
-- Then drop the column
|
||||||
|
ALTER TABLE [dbo].[Organization] DROP COLUMN [UseAdminSponsoredFamilies]
|
||||||
|
END
|
||||||
|
GO;
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[Organization] ADD [UseAdminSponsoredFamilies] bit NOT NULL CONSTRAINT [DF_Organization_UseAdminSponsoredFamilies] default (0)
|
||||||
|
GO;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[OrganizationSponsorship]') AND name = 'IsAdminInitiated')
|
||||||
|
BEGIN
|
||||||
|
-- First drop the default constraint
|
||||||
|
DECLARE @ConstraintName nvarchar(200)
|
||||||
|
SELECT @ConstraintName = name FROM sys.default_constraints
|
||||||
|
WHERE parent_object_id = OBJECT_ID(N'[dbo].[OrganizationSponsorship]')
|
||||||
|
AND parent_column_id = (SELECT column_id FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[OrganizationSponsorship]') AND name = 'IsAdminInitiated')
|
||||||
|
|
||||||
|
IF @ConstraintName IS NOT NULL
|
||||||
|
EXEC('ALTER TABLE [dbo].[OrganizationSponsorship] DROP CONSTRAINT ' + @ConstraintName)
|
||||||
|
|
||||||
|
-- Then drop the column
|
||||||
|
ALTER TABLE [dbo].[OrganizationSponsorship] DROP COLUMN [IsAdminInitiated]
|
||||||
|
END
|
||||||
|
GO;
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[OrganizationSponsorship] ADD [IsAdminInitiated] BIT CONSTRAINT [DF_OrganizationSponsorship_IsAdminInitiated] DEFAULT (0) NOT NULL
|
||||||
|
GO;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[OrganizationSponsorship]') AND name = 'Notes')
|
||||||
|
BEGIN
|
||||||
|
-- Notes column doesn't have a default constraint, so we can just drop it
|
||||||
|
ALTER TABLE [dbo].[OrganizationSponsorship] DROP COLUMN [Notes]
|
||||||
|
END
|
||||||
|
GO;
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[OrganizationSponsorship] ADD [Notes] NVARCHAR(512) NULL
|
||||||
|
GO;
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Organization_Create]
|
||||||
|
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||||
|
@Identifier NVARCHAR(50),
|
||||||
|
@Name NVARCHAR(50),
|
||||||
|
@BusinessName NVARCHAR(50),
|
||||||
|
@BusinessAddress1 NVARCHAR(50),
|
||||||
|
@BusinessAddress2 NVARCHAR(50),
|
||||||
|
@BusinessAddress3 NVARCHAR(50),
|
||||||
|
@BusinessCountry VARCHAR(2),
|
||||||
|
@BusinessTaxNumber NVARCHAR(30),
|
||||||
|
@BillingEmail NVARCHAR(256),
|
||||||
|
@Plan NVARCHAR(50),
|
||||||
|
@PlanType TINYINT,
|
||||||
|
@Seats INT,
|
||||||
|
@MaxCollections SMALLINT,
|
||||||
|
@UsePolicies BIT,
|
||||||
|
@UseSso BIT,
|
||||||
|
@UseGroups BIT,
|
||||||
|
@UseDirectory BIT,
|
||||||
|
@UseEvents BIT,
|
||||||
|
@UseTotp BIT,
|
||||||
|
@Use2fa BIT,
|
||||||
|
@UseApi BIT,
|
||||||
|
@UseResetPassword BIT,
|
||||||
|
@SelfHost BIT,
|
||||||
|
@UsersGetPremium BIT,
|
||||||
|
@Storage BIGINT,
|
||||||
|
@MaxStorageGb SMALLINT,
|
||||||
|
@Gateway TINYINT,
|
||||||
|
@GatewayCustomerId VARCHAR(50),
|
||||||
|
@GatewaySubscriptionId VARCHAR(50),
|
||||||
|
@ReferenceData VARCHAR(MAX),
|
||||||
|
@Enabled BIT,
|
||||||
|
@LicenseKey VARCHAR(100),
|
||||||
|
@PublicKey VARCHAR(MAX),
|
||||||
|
@PrivateKey VARCHAR(MAX),
|
||||||
|
@TwoFactorProviders NVARCHAR(MAX),
|
||||||
|
@ExpirationDate DATETIME2(7),
|
||||||
|
@CreationDate DATETIME2(7),
|
||||||
|
@RevisionDate DATETIME2(7),
|
||||||
|
@OwnersNotifiedOfAutoscaling DATETIME2(7),
|
||||||
|
@MaxAutoscaleSeats INT,
|
||||||
|
@UseKeyConnector BIT = 0,
|
||||||
|
@UseScim BIT = 0,
|
||||||
|
@UseCustomPermissions BIT = 0,
|
||||||
|
@UseSecretsManager BIT = 0,
|
||||||
|
@Status TINYINT = 0,
|
||||||
|
@UsePasswordManager BIT = 1,
|
||||||
|
@SmSeats INT = null,
|
||||||
|
@SmServiceAccounts INT = null,
|
||||||
|
@MaxAutoscaleSmSeats INT= null,
|
||||||
|
@MaxAutoscaleSmServiceAccounts INT = null,
|
||||||
|
@SecretsManagerBeta BIT = 0,
|
||||||
|
@LimitCollectionCreation BIT = NULL,
|
||||||
|
@LimitCollectionDeletion BIT = NULL,
|
||||||
|
@AllowAdminAccessToAllCollectionItems BIT = 0,
|
||||||
|
@UseRiskInsights BIT = 0,
|
||||||
|
@LimitItemDeletion BIT = 0,
|
||||||
|
@UseAdminSponsoredFamilies BIT = 0
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
INSERT INTO [dbo].[Organization]
|
||||||
|
(
|
||||||
|
[Id],
|
||||||
|
[Identifier],
|
||||||
|
[Name],
|
||||||
|
[BusinessName],
|
||||||
|
[BusinessAddress1],
|
||||||
|
[BusinessAddress2],
|
||||||
|
[BusinessAddress3],
|
||||||
|
[BusinessCountry],
|
||||||
|
[BusinessTaxNumber],
|
||||||
|
[BillingEmail],
|
||||||
|
[Plan],
|
||||||
|
[PlanType],
|
||||||
|
[Seats],
|
||||||
|
[MaxCollections],
|
||||||
|
[UsePolicies],
|
||||||
|
[UseSso],
|
||||||
|
[UseGroups],
|
||||||
|
[UseDirectory],
|
||||||
|
[UseEvents],
|
||||||
|
[UseTotp],
|
||||||
|
[Use2fa],
|
||||||
|
[UseApi],
|
||||||
|
[UseResetPassword],
|
||||||
|
[SelfHost],
|
||||||
|
[UsersGetPremium],
|
||||||
|
[Storage],
|
||||||
|
[MaxStorageGb],
|
||||||
|
[Gateway],
|
||||||
|
[GatewayCustomerId],
|
||||||
|
[GatewaySubscriptionId],
|
||||||
|
[ReferenceData],
|
||||||
|
[Enabled],
|
||||||
|
[LicenseKey],
|
||||||
|
[PublicKey],
|
||||||
|
[PrivateKey],
|
||||||
|
[TwoFactorProviders],
|
||||||
|
[ExpirationDate],
|
||||||
|
[CreationDate],
|
||||||
|
[RevisionDate],
|
||||||
|
[OwnersNotifiedOfAutoscaling],
|
||||||
|
[MaxAutoscaleSeats],
|
||||||
|
[UseKeyConnector],
|
||||||
|
[UseScim],
|
||||||
|
[UseCustomPermissions],
|
||||||
|
[UseSecretsManager],
|
||||||
|
[Status],
|
||||||
|
[UsePasswordManager],
|
||||||
|
[SmSeats],
|
||||||
|
[SmServiceAccounts],
|
||||||
|
[MaxAutoscaleSmSeats],
|
||||||
|
[MaxAutoscaleSmServiceAccounts],
|
||||||
|
[SecretsManagerBeta],
|
||||||
|
[LimitCollectionCreation],
|
||||||
|
[LimitCollectionDeletion],
|
||||||
|
[AllowAdminAccessToAllCollectionItems],
|
||||||
|
[UseRiskInsights],
|
||||||
|
[LimitItemDeletion],
|
||||||
|
[UseAdminSponsoredFamilies]
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@Id,
|
||||||
|
@Identifier,
|
||||||
|
@Name,
|
||||||
|
@BusinessName,
|
||||||
|
@BusinessAddress1,
|
||||||
|
@BusinessAddress2,
|
||||||
|
@BusinessAddress3,
|
||||||
|
@BusinessCountry,
|
||||||
|
@BusinessTaxNumber,
|
||||||
|
@BillingEmail,
|
||||||
|
@Plan,
|
||||||
|
@PlanType,
|
||||||
|
@Seats,
|
||||||
|
@MaxCollections,
|
||||||
|
@UsePolicies,
|
||||||
|
@UseSso,
|
||||||
|
@UseGroups,
|
||||||
|
@UseDirectory,
|
||||||
|
@UseEvents,
|
||||||
|
@UseTotp,
|
||||||
|
@Use2fa,
|
||||||
|
@UseApi,
|
||||||
|
@UseResetPassword,
|
||||||
|
@SelfHost,
|
||||||
|
@UsersGetPremium,
|
||||||
|
@Storage,
|
||||||
|
@MaxStorageGb,
|
||||||
|
@Gateway,
|
||||||
|
@GatewayCustomerId,
|
||||||
|
@GatewaySubscriptionId,
|
||||||
|
@ReferenceData,
|
||||||
|
@Enabled,
|
||||||
|
@LicenseKey,
|
||||||
|
@PublicKey,
|
||||||
|
@PrivateKey,
|
||||||
|
@TwoFactorProviders,
|
||||||
|
@ExpirationDate,
|
||||||
|
@CreationDate,
|
||||||
|
@RevisionDate,
|
||||||
|
@OwnersNotifiedOfAutoscaling,
|
||||||
|
@MaxAutoscaleSeats,
|
||||||
|
@UseKeyConnector,
|
||||||
|
@UseScim,
|
||||||
|
@UseCustomPermissions,
|
||||||
|
@UseSecretsManager,
|
||||||
|
@Status,
|
||||||
|
@UsePasswordManager,
|
||||||
|
@SmSeats,
|
||||||
|
@SmServiceAccounts,
|
||||||
|
@MaxAutoscaleSmSeats,
|
||||||
|
@MaxAutoscaleSmServiceAccounts,
|
||||||
|
@SecretsManagerBeta,
|
||||||
|
@LimitCollectionCreation,
|
||||||
|
@LimitCollectionDeletion,
|
||||||
|
@AllowAdminAccessToAllCollectionItems,
|
||||||
|
@UseRiskInsights,
|
||||||
|
@LimitItemDeletion,
|
||||||
|
@UseAdminSponsoredFamilies
|
||||||
|
)
|
||||||
|
END
|
||||||
|
GO;
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Organization_ReadAbilities]
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
[Id],
|
||||||
|
[UseEvents],
|
||||||
|
[Use2fa],
|
||||||
|
CASE
|
||||||
|
WHEN [Use2fa] = 1 AND [TwoFactorProviders] IS NOT NULL AND [TwoFactorProviders] != '{}' THEN
|
||||||
|
1
|
||||||
|
ELSE
|
||||||
|
0
|
||||||
|
END AS [Using2fa],
|
||||||
|
[UsersGetPremium],
|
||||||
|
[UseCustomPermissions],
|
||||||
|
[UseSso],
|
||||||
|
[UseKeyConnector],
|
||||||
|
[UseScim],
|
||||||
|
[UseResetPassword],
|
||||||
|
[UsePolicies],
|
||||||
|
[Enabled],
|
||||||
|
[LimitCollectionCreation],
|
||||||
|
[LimitCollectionDeletion],
|
||||||
|
[AllowAdminAccessToAllCollectionItems],
|
||||||
|
[UseRiskInsights],
|
||||||
|
[LimitItemDeletion],
|
||||||
|
[UseAdminSponsoredFamilies]
|
||||||
|
FROM
|
||||||
|
[dbo].[Organization]
|
||||||
|
END
|
||||||
|
GO;
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Organization_Update]
|
||||||
|
@Id UNIQUEIDENTIFIER,
|
||||||
|
@Identifier NVARCHAR(50),
|
||||||
|
@Name NVARCHAR(50),
|
||||||
|
@BusinessName NVARCHAR(50),
|
||||||
|
@BusinessAddress1 NVARCHAR(50),
|
||||||
|
@BusinessAddress2 NVARCHAR(50),
|
||||||
|
@BusinessAddress3 NVARCHAR(50),
|
||||||
|
@BusinessCountry VARCHAR(2),
|
||||||
|
@BusinessTaxNumber NVARCHAR(30),
|
||||||
|
@BillingEmail NVARCHAR(256),
|
||||||
|
@Plan NVARCHAR(50),
|
||||||
|
@PlanType TINYINT,
|
||||||
|
@Seats INT,
|
||||||
|
@MaxCollections SMALLINT,
|
||||||
|
@UsePolicies BIT,
|
||||||
|
@UseSso BIT,
|
||||||
|
@UseGroups BIT,
|
||||||
|
@UseDirectory BIT,
|
||||||
|
@UseEvents BIT,
|
||||||
|
@UseTotp BIT,
|
||||||
|
@Use2fa BIT,
|
||||||
|
@UseApi BIT,
|
||||||
|
@UseResetPassword BIT,
|
||||||
|
@SelfHost BIT,
|
||||||
|
@UsersGetPremium BIT,
|
||||||
|
@Storage BIGINT,
|
||||||
|
@MaxStorageGb SMALLINT,
|
||||||
|
@Gateway TINYINT,
|
||||||
|
@GatewayCustomerId VARCHAR(50),
|
||||||
|
@GatewaySubscriptionId VARCHAR(50),
|
||||||
|
@ReferenceData VARCHAR(MAX),
|
||||||
|
@Enabled BIT,
|
||||||
|
@LicenseKey VARCHAR(100),
|
||||||
|
@PublicKey VARCHAR(MAX),
|
||||||
|
@PrivateKey VARCHAR(MAX),
|
||||||
|
@TwoFactorProviders NVARCHAR(MAX),
|
||||||
|
@ExpirationDate DATETIME2(7),
|
||||||
|
@CreationDate DATETIME2(7),
|
||||||
|
@RevisionDate DATETIME2(7),
|
||||||
|
@OwnersNotifiedOfAutoscaling DATETIME2(7),
|
||||||
|
@MaxAutoscaleSeats INT,
|
||||||
|
@UseKeyConnector BIT = 0,
|
||||||
|
@UseScim BIT = 0,
|
||||||
|
@UseCustomPermissions BIT = 0,
|
||||||
|
@UseSecretsManager BIT = 0,
|
||||||
|
@Status TINYINT = 0,
|
||||||
|
@UsePasswordManager BIT = 1,
|
||||||
|
@SmSeats INT = null,
|
||||||
|
@SmServiceAccounts INT = null,
|
||||||
|
@MaxAutoscaleSmSeats INT = null,
|
||||||
|
@MaxAutoscaleSmServiceAccounts INT = null,
|
||||||
|
@SecretsManagerBeta BIT = 0,
|
||||||
|
@LimitCollectionCreation BIT = null,
|
||||||
|
@LimitCollectionDeletion BIT = null,
|
||||||
|
@AllowAdminAccessToAllCollectionItems BIT = 0,
|
||||||
|
@UseRiskInsights BIT = 0,
|
||||||
|
@LimitItemDeletion BIT = 0,
|
||||||
|
@UseAdminSponsoredFamilies BIT = 0
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
UPDATE
|
||||||
|
[dbo].[Organization]
|
||||||
|
SET
|
||||||
|
[Identifier] = @Identifier,
|
||||||
|
[Name] = @Name,
|
||||||
|
[BusinessName] = @BusinessName,
|
||||||
|
[BusinessAddress1] = @BusinessAddress1,
|
||||||
|
[BusinessAddress2] = @BusinessAddress2,
|
||||||
|
[BusinessAddress3] = @BusinessAddress3,
|
||||||
|
[BusinessCountry] = @BusinessCountry,
|
||||||
|
[BusinessTaxNumber] = @BusinessTaxNumber,
|
||||||
|
[BillingEmail] = @BillingEmail,
|
||||||
|
[Plan] = @Plan,
|
||||||
|
[PlanType] = @PlanType,
|
||||||
|
[Seats] = @Seats,
|
||||||
|
[MaxCollections] = @MaxCollections,
|
||||||
|
[UsePolicies] = @UsePolicies,
|
||||||
|
[UseSso] = @UseSso,
|
||||||
|
[UseGroups] = @UseGroups,
|
||||||
|
[UseDirectory] = @UseDirectory,
|
||||||
|
[UseEvents] = @UseEvents,
|
||||||
|
[UseTotp] = @UseTotp,
|
||||||
|
[Use2fa] = @Use2fa,
|
||||||
|
[UseApi] = @UseApi,
|
||||||
|
[UseResetPassword] = @UseResetPassword,
|
||||||
|
[SelfHost] = @SelfHost,
|
||||||
|
[UsersGetPremium] = @UsersGetPremium,
|
||||||
|
[Storage] = @Storage,
|
||||||
|
[MaxStorageGb] = @MaxStorageGb,
|
||||||
|
[Gateway] = @Gateway,
|
||||||
|
[GatewayCustomerId] = @GatewayCustomerId,
|
||||||
|
[GatewaySubscriptionId] = @GatewaySubscriptionId,
|
||||||
|
[ReferenceData] = @ReferenceData,
|
||||||
|
[Enabled] = @Enabled,
|
||||||
|
[LicenseKey] = @LicenseKey,
|
||||||
|
[PublicKey] = @PublicKey,
|
||||||
|
[PrivateKey] = @PrivateKey,
|
||||||
|
[TwoFactorProviders] = @TwoFactorProviders,
|
||||||
|
[ExpirationDate] = @ExpirationDate,
|
||||||
|
[CreationDate] = @CreationDate,
|
||||||
|
[RevisionDate] = @RevisionDate,
|
||||||
|
[OwnersNotifiedOfAutoscaling] = @OwnersNotifiedOfAutoscaling,
|
||||||
|
[MaxAutoscaleSeats] = @MaxAutoscaleSeats,
|
||||||
|
[UseKeyConnector] = @UseKeyConnector,
|
||||||
|
[UseScim] = @UseScim,
|
||||||
|
[UseCustomPermissions] = @UseCustomPermissions,
|
||||||
|
[UseSecretsManager] = @UseSecretsManager,
|
||||||
|
[Status] = @Status,
|
||||||
|
[UsePasswordManager] = @UsePasswordManager,
|
||||||
|
[SmSeats] = @SmSeats,
|
||||||
|
[SmServiceAccounts] = @SmServiceAccounts,
|
||||||
|
[MaxAutoscaleSmSeats] = @MaxAutoscaleSmSeats,
|
||||||
|
[MaxAutoscaleSmServiceAccounts] = @MaxAutoscaleSmServiceAccounts,
|
||||||
|
[SecretsManagerBeta] = @SecretsManagerBeta,
|
||||||
|
[LimitCollectionCreation] = @LimitCollectionCreation,
|
||||||
|
[LimitCollectionDeletion] = @LimitCollectionDeletion,
|
||||||
|
[AllowAdminAccessToAllCollectionItems] = @AllowAdminAccessToAllCollectionItems,
|
||||||
|
[UseRiskInsights] = @UseRiskInsights,
|
||||||
|
[LimitItemDeletion] = @LimitItemDeletion,
|
||||||
|
[UseAdminSponsoredFamilies] = @UseAdminSponsoredFamilies
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
END
|
||||||
|
GO;
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[OrganizationSponsorship_Update]
|
||||||
|
@Id UNIQUEIDENTIFIER,
|
||||||
|
@SponsoringOrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@SponsoringOrganizationUserID UNIQUEIDENTIFIER,
|
||||||
|
@SponsoredOrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@FriendlyName NVARCHAR(256),
|
||||||
|
@OfferedToEmail NVARCHAR(256),
|
||||||
|
@PlanSponsorshipType TINYINT,
|
||||||
|
@ToDelete BIT,
|
||||||
|
@LastSyncDate DATETIME2 (7),
|
||||||
|
@ValidUntil DATETIME2 (7),
|
||||||
|
@IsAdminInitiated BIT = 0,
|
||||||
|
@Notes NVARCHAR(512) = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
UPDATE
|
||||||
|
[dbo].[OrganizationSponsorship]
|
||||||
|
SET
|
||||||
|
[SponsoringOrganizationId] = @SponsoringOrganizationId,
|
||||||
|
[SponsoringOrganizationUserID] = @SponsoringOrganizationUserID,
|
||||||
|
[SponsoredOrganizationId] = @SponsoredOrganizationId,
|
||||||
|
[FriendlyName] = @FriendlyName,
|
||||||
|
[OfferedToEmail] = @OfferedToEmail,
|
||||||
|
[PlanSponsorshipType] = @PlanSponsorshipType,
|
||||||
|
[ToDelete] = @ToDelete,
|
||||||
|
[LastSyncDate] = @LastSyncDate,
|
||||||
|
[ValidUntil] = @ValidUntil,
|
||||||
|
[IsAdminInitiated] = @IsAdminInitiated,
|
||||||
|
[Notes] = @Notes
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
END
|
||||||
|
GO;
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[OrganizationSponsorship_Create]
|
||||||
|
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||||
|
@SponsoringOrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@SponsoringOrganizationUserID UNIQUEIDENTIFIER,
|
||||||
|
@SponsoredOrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@FriendlyName NVARCHAR(256),
|
||||||
|
@OfferedToEmail NVARCHAR(256),
|
||||||
|
@PlanSponsorshipType TINYINT,
|
||||||
|
@ToDelete BIT,
|
||||||
|
@LastSyncDate DATETIME2 (7),
|
||||||
|
@ValidUntil DATETIME2 (7),
|
||||||
|
@IsAdminInitiated BIT = 0,
|
||||||
|
@Notes NVARCHAR(512) = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
INSERT INTO [dbo].[OrganizationSponsorship]
|
||||||
|
(
|
||||||
|
[Id],
|
||||||
|
[SponsoringOrganizationId],
|
||||||
|
[SponsoringOrganizationUserID],
|
||||||
|
[SponsoredOrganizationId],
|
||||||
|
[FriendlyName],
|
||||||
|
[OfferedToEmail],
|
||||||
|
[PlanSponsorshipType],
|
||||||
|
[ToDelete],
|
||||||
|
[LastSyncDate],
|
||||||
|
[ValidUntil],
|
||||||
|
[IsAdminInitiated],
|
||||||
|
[Notes]
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@Id,
|
||||||
|
@SponsoringOrganizationId,
|
||||||
|
@SponsoringOrganizationUserID,
|
||||||
|
@SponsoredOrganizationId,
|
||||||
|
@FriendlyName,
|
||||||
|
@OfferedToEmail,
|
||||||
|
@PlanSponsorshipType,
|
||||||
|
@ToDelete,
|
||||||
|
@LastSyncDate,
|
||||||
|
@ValidUntil,
|
||||||
|
@IsAdminInitiated,
|
||||||
|
@Notes
|
||||||
|
)
|
||||||
|
END
|
||||||
|
GO;
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS [dbo].[OrganizationSponsorship_CreateMany];
|
||||||
|
DROP PROCEDURE IF EXISTS [dbo].[OrganizationSponsorship_UpdateMany];
|
||||||
|
DROP TYPE IF EXISTS [dbo].[OrganizationSponsorshipType] GO;
|
||||||
|
|
||||||
|
CREATE TYPE [dbo].[OrganizationSponsorshipType] AS TABLE(
|
||||||
|
[Id] UNIQUEIDENTIFIER,
|
||||||
|
[SponsoringOrganizationId] UNIQUEIDENTIFIER,
|
||||||
|
[SponsoringOrganizationUserID] UNIQUEIDENTIFIER,
|
||||||
|
[SponsoredOrganizationId] UNIQUEIDENTIFIER,
|
||||||
|
[FriendlyName] NVARCHAR(256),
|
||||||
|
[OfferedToEmail] VARCHAR(256),
|
||||||
|
[PlanSponsorshipType] TINYINT,
|
||||||
|
[LastSyncDate] DATETIME2(7),
|
||||||
|
[ValidUntil] DATETIME2(7),
|
||||||
|
[ToDelete] BIT,
|
||||||
|
[IsAdminInitiated] BIT DEFAULT 0,
|
||||||
|
[Notes] NVARCHAR(512) NULL
|
||||||
|
);
|
||||||
|
GO;
|
||||||
|
|
||||||
|
CREATE PROCEDURE [dbo].[OrganizationSponsorship_CreateMany]
|
||||||
|
@OrganizationSponsorshipsInput [dbo].[OrganizationSponsorshipType] READONLY
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
INSERT INTO [dbo].[OrganizationSponsorship]
|
||||||
|
(
|
||||||
|
[Id],
|
||||||
|
[SponsoringOrganizationId],
|
||||||
|
[SponsoringOrganizationUserID],
|
||||||
|
[SponsoredOrganizationId],
|
||||||
|
[FriendlyName],
|
||||||
|
[OfferedToEmail],
|
||||||
|
[PlanSponsorshipType],
|
||||||
|
[ToDelete],
|
||||||
|
[LastSyncDate],
|
||||||
|
[ValidUntil],
|
||||||
|
[IsAdminInitiated],
|
||||||
|
[Notes]
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
OS.[Id],
|
||||||
|
OS.[SponsoringOrganizationId],
|
||||||
|
OS.[SponsoringOrganizationUserID],
|
||||||
|
OS.[SponsoredOrganizationId],
|
||||||
|
OS.[FriendlyName],
|
||||||
|
OS.[OfferedToEmail],
|
||||||
|
OS.[PlanSponsorshipType],
|
||||||
|
OS.[ToDelete],
|
||||||
|
OS.[LastSyncDate],
|
||||||
|
OS.[ValidUntil],
|
||||||
|
OS.[IsAdminInitiated],
|
||||||
|
OS.[Notes]
|
||||||
|
FROM
|
||||||
|
@OrganizationSponsorshipsInput OS
|
||||||
|
END
|
||||||
|
GO;
|
||||||
|
|
||||||
|
CREATE PROCEDURE [dbo].[OrganizationSponsorship_UpdateMany]
|
||||||
|
@OrganizationSponsorshipsInput [dbo].[OrganizationSponsorshipType] READONLY
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
UPDATE
|
||||||
|
OS
|
||||||
|
SET
|
||||||
|
[Id] = OSI.[Id],
|
||||||
|
[SponsoringOrganizationId] = OSI.[SponsoringOrganizationId],
|
||||||
|
[SponsoringOrganizationUserID] = OSI.[SponsoringOrganizationUserID],
|
||||||
|
[SponsoredOrganizationId] = OSI.[SponsoredOrganizationId],
|
||||||
|
[FriendlyName] = OSI.[FriendlyName],
|
||||||
|
[OfferedToEmail] = OSI.[OfferedToEmail],
|
||||||
|
[PlanSponsorshipType] = OSI.[PlanSponsorshipType],
|
||||||
|
[ToDelete] = OSI.[ToDelete],
|
||||||
|
[LastSyncDate] = OSI.[LastSyncDate],
|
||||||
|
[ValidUntil] = OSI.[ValidUntil],
|
||||||
|
[IsAdminInitiated] = OSI.[IsAdminInitiated],
|
||||||
|
[Notes] = OSI.[Notes]
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationSponsorship] OS
|
||||||
|
INNER JOIN
|
||||||
|
@OrganizationSponsorshipsInput OSI ON OS.Id = OSI.Id
|
||||||
|
|
||||||
|
END
|
||||||
|
GO;
|
Loading…
x
Reference in New Issue
Block a user