mirror of
https://github.com/bitwarden/server.git
synced 2026-02-04 02:05:30 -06:00
Merge branch 'main' into billing/PM-31140/consolidate-unpaid-subscription-handling
Resolved conflict in SubscriptionUpdatedHandlerTests.cs by using Stripe.Plan explicit type qualification from main.
This commit is contained in:
commit
04104b3d46
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@ -97,9 +97,7 @@ src/Admin/Views/Tools @bitwarden/team-billing-dev
|
|||||||
.github/workflows/test-database.yml @bitwarden/team-platform-dev
|
.github/workflows/test-database.yml @bitwarden/team-platform-dev
|
||||||
.github/workflows/test.yml @bitwarden/team-platform-dev
|
.github/workflows/test.yml @bitwarden/team-platform-dev
|
||||||
**/*Platform* @bitwarden/team-platform-dev
|
**/*Platform* @bitwarden/team-platform-dev
|
||||||
**/.dockerignore @bitwarden/team-platform-dev
|
|
||||||
**/Dockerfile @bitwarden/team-platform-dev
|
|
||||||
**/entrypoint.sh @bitwarden/team-platform-dev
|
|
||||||
# The PushType enum is expected to be editted by anyone without need for Platform review
|
# The PushType enum is expected to be editted by anyone without need for Platform review
|
||||||
src/Core/Platform/Push/PushType.cs
|
src/Core/Platform/Push/PushType.cs
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -263,7 +263,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Scan Docker image
|
- name: Scan Docker image
|
||||||
id: container-scan
|
id: container-scan
|
||||||
uses: anchore/scan-action@62b74fb7bb810d2c45b1865f47a77655621862a5 # v7.2.3
|
uses: anchore/scan-action@0d444ed77d83ee2ba7f5ced0d90d640a1281d762 # v7.3.0
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.image-tags.outputs.primary_tag }}
|
image: ${{ steps.image-tags.outputs.primary_tag }}
|
||||||
fail-build: false
|
fail-build: false
|
||||||
|
|||||||
@ -13,6 +13,10 @@
|
|||||||
<TreatWarningsAsErrors Condition="'$(TreatWarningsAsErrors)' == ''">true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors Condition="'$(TreatWarningsAsErrors)' == ''">true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<BitIncludeAuthentication>false</BitIncludeAuthentication>
|
||||||
|
<BitIncludeFeatures>false</BitIncludeFeatures>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
<Sdk Name="Bitwarden.Server.Sdk" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<UserSecretsId>bitwarden-Scim</UserSecretsId>
|
<UserSecretsId>bitwarden-Scim</UserSecretsId>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
using Bit.Core;
|
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.OrganizationFeatures.Policies;
|
||||||
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;
|
using Bit.Core.Auth.Models;
|
||||||
@ -45,7 +45,7 @@ public class AccountController : Controller
|
|||||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||||
private readonly ISsoUserRepository _ssoUserRepository;
|
private readonly ISsoUserRepository _ssoUserRepository;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyQuery _policyQuery;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly II18nService _i18nService;
|
private readonly II18nService _i18nService;
|
||||||
private readonly UserManager<User> _userManager;
|
private readonly UserManager<User> _userManager;
|
||||||
@ -67,7 +67,7 @@ public class AccountController : Controller
|
|||||||
ISsoConfigRepository ssoConfigRepository,
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
ISsoUserRepository ssoUserRepository,
|
ISsoUserRepository ssoUserRepository,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyQuery policyQuery,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
II18nService i18nService,
|
II18nService i18nService,
|
||||||
UserManager<User> userManager,
|
UserManager<User> userManager,
|
||||||
@ -88,7 +88,7 @@ public class AccountController : Controller
|
|||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_ssoConfigRepository = ssoConfigRepository;
|
_ssoConfigRepository = ssoConfigRepository;
|
||||||
_ssoUserRepository = ssoUserRepository;
|
_ssoUserRepository = ssoUserRepository;
|
||||||
_policyRepository = policyRepository;
|
_policyQuery = policyQuery;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_i18nService = i18nService;
|
_i18nService = i18nService;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
@ -687,9 +687,8 @@ public class AccountController : Controller
|
|||||||
await _registerUserCommand.RegisterSSOAutoProvisionedUserAsync(newUser, organization);
|
await _registerUserCommand.RegisterSSOAutoProvisionedUserAsync(newUser, organization);
|
||||||
|
|
||||||
// If the organization has 2fa policy enabled, make sure to default jit user 2fa to email
|
// If the organization has 2fa policy enabled, make sure to default jit user 2fa to email
|
||||||
var twoFactorPolicy =
|
var twoFactorPolicy = await _policyQuery.RunAsync(organization.Id, PolicyType.TwoFactorAuthentication);
|
||||||
await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.TwoFactorAuthentication);
|
if (twoFactorPolicy.Enabled)
|
||||||
if (twoFactorPolicy != null && twoFactorPolicy.Enabled)
|
|
||||||
{
|
{
|
||||||
newUser.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
newUser.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
<Sdk Name="Bitwarden.Server.Sdk" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<UserSecretsId>bitwarden-Sso</UserSecretsId>
|
<UserSecretsId>bitwarden-Sso</UserSecretsId>
|
||||||
|
|||||||
@ -8,7 +8,6 @@ using Bit.Core.Utilities;
|
|||||||
using Bit.SharedWeb.Utilities;
|
using Bit.SharedWeb.Utilities;
|
||||||
using Bit.Sso.Utilities;
|
using Bit.Sso.Utilities;
|
||||||
using Duende.IdentityServer.Services;
|
using Duende.IdentityServer.Services;
|
||||||
using Microsoft.IdentityModel.Logging;
|
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Sso;
|
namespace Bit.Sso;
|
||||||
@ -91,20 +90,15 @@ public class Startup
|
|||||||
|
|
||||||
public void Configure(
|
public void Configure(
|
||||||
IApplicationBuilder app,
|
IApplicationBuilder app,
|
||||||
IWebHostEnvironment env,
|
IWebHostEnvironment environment,
|
||||||
IHostApplicationLifetime appLifetime,
|
IHostApplicationLifetime appLifetime,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
ILogger<Startup> logger)
|
ILogger<Startup> logger)
|
||||||
{
|
{
|
||||||
if (env.IsDevelopment() || globalSettings.SelfHosted)
|
|
||||||
{
|
|
||||||
IdentityModelEventSource.ShowPII = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add general security headers
|
// Add general security headers
|
||||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||||
|
|
||||||
if (!env.IsDevelopment())
|
if (!environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
var uri = new Uri(globalSettings.BaseServiceUri.Sso);
|
var uri = new Uri(globalSettings.BaseServiceUri.Sso);
|
||||||
app.Use(async (ctx, next) =>
|
app.Use(async (ctx, next) =>
|
||||||
@ -120,7 +114,7 @@ public class Startup
|
|||||||
app.UseForwardedHeaders(globalSettings);
|
app.UseForwardedHeaders(globalSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.IsDevelopment())
|
if (environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
app.UseCookiePolicy();
|
app.UseCookiePolicy();
|
||||||
|
|||||||
@ -28,6 +28,7 @@ $projects = @{
|
|||||||
Scim = "../bitwarden_license/src/Scim"
|
Scim = "../bitwarden_license/src/Scim"
|
||||||
IntegrationTests = "../test/Infrastructure.IntegrationTest"
|
IntegrationTests = "../test/Infrastructure.IntegrationTest"
|
||||||
SeederApi = "../util/SeederApi"
|
SeederApi = "../util/SeederApi"
|
||||||
|
SeederUtility = "../util/DbSeederUtility"
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($key in $projects.keys) {
|
foreach ($key in $projects.keys) {
|
||||||
|
|||||||
@ -6,6 +6,6 @@
|
|||||||
"msbuild-sdks": {
|
"msbuild-sdks": {
|
||||||
"Microsoft.Build.Traversal": "4.1.0",
|
"Microsoft.Build.Traversal": "4.1.0",
|
||||||
"Microsoft.Build.Sql": "1.0.0",
|
"Microsoft.Build.Sql": "1.0.0",
|
||||||
"Bitwarden.Server.Sdk": "1.2.0"
|
"Bitwarden.Server.Sdk": "1.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
<Sdk Name="Bitwarden.Server.Sdk" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<UserSecretsId>bitwarden-Admin</UserSecretsId>
|
<UserSecretsId>bitwarden-Admin</UserSecretsId>
|
||||||
|
|||||||
@ -86,7 +86,7 @@ public class UsersController : Controller
|
|||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
||||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
|
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id, withOrganizations: false);
|
||||||
|
|
||||||
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id);
|
var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id);
|
||||||
|
|||||||
@ -8,7 +8,7 @@ public class Program
|
|||||||
{
|
{
|
||||||
Host
|
Host
|
||||||
.CreateDefaultBuilder(args)
|
.CreateDefaultBuilder(args)
|
||||||
.ConfigureCustomAppConfiguration(args)
|
.UseBitwardenSdk()
|
||||||
.ConfigureWebHostDefaults(webBuilder =>
|
.ConfigureWebHostDefaults(webBuilder =>
|
||||||
{
|
{
|
||||||
webBuilder.ConfigureKestrel(o =>
|
webBuilder.ConfigureKestrel(o =>
|
||||||
|
|||||||
@ -57,7 +57,7 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
|||||||
private readonly ICollectionRepository _collectionRepository;
|
private readonly ICollectionRepository _collectionRepository;
|
||||||
private readonly IGroupRepository _groupRepository;
|
private readonly IGroupRepository _groupRepository;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyQuery _policyQuery;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;
|
private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;
|
||||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||||
@ -90,7 +90,7 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
|||||||
ICollectionRepository collectionRepository,
|
ICollectionRepository collectionRepository,
|
||||||
IGroupRepository groupRepository,
|
IGroupRepository groupRepository,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyQuery policyQuery,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery,
|
ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery,
|
||||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||||
@ -123,7 +123,7 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
|||||||
_collectionRepository = collectionRepository;
|
_collectionRepository = collectionRepository;
|
||||||
_groupRepository = groupRepository;
|
_groupRepository = groupRepository;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_policyRepository = policyRepository;
|
_policyQuery = policyQuery;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;
|
_countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;
|
||||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||||
@ -350,10 +350,9 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var masterPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
var masterPasswordPolicy = await _policyQuery.RunAsync(orgId, PolicyType.ResetPassword);
|
||||||
var useMasterPasswordPolicy = masterPasswordPolicy != null &&
|
var useMasterPasswordPolicy = masterPasswordPolicy.Enabled &&
|
||||||
masterPasswordPolicy.Enabled &&
|
masterPasswordPolicy.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled;
|
||||||
masterPasswordPolicy.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled;
|
|
||||||
|
|
||||||
return useMasterPasswordPolicy;
|
return useMasterPasswordPolicy;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,7 +48,7 @@ public class OrganizationsController : Controller
|
|||||||
{
|
{
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyQuery _policyQuery;
|
||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
@ -74,7 +74,7 @@ public class OrganizationsController : Controller
|
|||||||
public OrganizationsController(
|
public OrganizationsController(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyQuery policyQuery,
|
||||||
IOrganizationService organizationService,
|
IOrganizationService organizationService,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@ -99,7 +99,7 @@ public class OrganizationsController : Controller
|
|||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_policyRepository = policyRepository;
|
_policyQuery = policyQuery;
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
@ -183,15 +183,14 @@ public class OrganizationsController : Controller
|
|||||||
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, resetPasswordPolicyRequirement.AutoEnrollEnabled(organization.Id));
|
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, resetPasswordPolicyRequirement.AutoEnrollEnabled(organization.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
var resetPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
|
var resetPasswordPolicy = await _policyQuery.RunAsync(organization.Id, PolicyType.ResetPassword);
|
||||||
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled || resetPasswordPolicy.Data == null)
|
if (!resetPasswordPolicy.Enabled || resetPasswordPolicy.Data == null)
|
||||||
{
|
{
|
||||||
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, false);
|
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = JsonSerializer.Deserialize<ResetPasswordDataModel>(resetPasswordPolicy.Data, JsonHelpers.IgnoreCase);
|
var data = JsonSerializer.Deserialize<ResetPasswordDataModel>(resetPasswordPolicy.Data, JsonHelpers.IgnoreCase);
|
||||||
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false);
|
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("")]
|
[HttpPost("")]
|
||||||
|
|||||||
@ -7,7 +7,6 @@ using Bit.Api.AdminConsole.Models.Request;
|
|||||||
using Bit.Api.AdminConsole.Models.Response.Helpers;
|
using Bit.Api.AdminConsole.Models.Response.Helpers;
|
||||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
@ -43,6 +42,7 @@ public class PoliciesController : Controller
|
|||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly ISavePolicyCommand _savePolicyCommand;
|
private readonly ISavePolicyCommand _savePolicyCommand;
|
||||||
private readonly IVNextSavePolicyCommand _vNextSavePolicyCommand;
|
private readonly IVNextSavePolicyCommand _vNextSavePolicyCommand;
|
||||||
|
private readonly IPolicyQuery _policyQuery;
|
||||||
|
|
||||||
public PoliciesController(IPolicyRepository policyRepository,
|
public PoliciesController(IPolicyRepository policyRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
@ -54,7 +54,8 @@ public class PoliciesController : Controller
|
|||||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
|
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
ISavePolicyCommand savePolicyCommand,
|
ISavePolicyCommand savePolicyCommand,
|
||||||
IVNextSavePolicyCommand vNextSavePolicyCommand)
|
IVNextSavePolicyCommand vNextSavePolicyCommand,
|
||||||
|
IPolicyQuery policyQuery)
|
||||||
{
|
{
|
||||||
_policyRepository = policyRepository;
|
_policyRepository = policyRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@ -68,27 +69,24 @@ public class PoliciesController : Controller
|
|||||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||||
_savePolicyCommand = savePolicyCommand;
|
_savePolicyCommand = savePolicyCommand;
|
||||||
_vNextSavePolicyCommand = vNextSavePolicyCommand;
|
_vNextSavePolicyCommand = vNextSavePolicyCommand;
|
||||||
|
_policyQuery = policyQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{type}")]
|
[HttpGet("{type}")]
|
||||||
public async Task<PolicyDetailResponseModel> Get(Guid orgId, int type)
|
public async Task<PolicyStatusResponseModel> Get(Guid orgId, PolicyType type)
|
||||||
{
|
{
|
||||||
if (!await _currentContext.ManagePolicies(orgId))
|
if (!await _currentContext.ManagePolicies(orgId))
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type);
|
|
||||||
if (policy == null)
|
|
||||||
{
|
|
||||||
return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
var policy = await _policyQuery.RunAsync(orgId, type);
|
||||||
if (policy.Type is PolicyType.SingleOrg)
|
if (policy.Type is PolicyType.SingleOrg)
|
||||||
{
|
{
|
||||||
return await policy.GetSingleOrgPolicyDetailResponseAsync(_organizationHasVerifiedDomainsQuery);
|
return await policy.GetSingleOrgPolicyStatusResponseAsync(_organizationHasVerifiedDomainsQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PolicyDetailResponseModel(policy);
|
return new PolicyStatusResponseModel(policy);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Models.Response.Helpers;
|
namespace Bit.Api.AdminConsole.Models.Response.Helpers;
|
||||||
|
|
||||||
public static class PolicyDetailResponses
|
public static class PolicyStatusResponses
|
||||||
{
|
{
|
||||||
public static async Task<PolicyDetailResponseModel> GetSingleOrgPolicyDetailResponseAsync(this Policy policy, IOrganizationHasVerifiedDomainsQuery hasVerifiedDomainsQuery)
|
public static async Task<PolicyStatusResponseModel> GetSingleOrgPolicyStatusResponseAsync(
|
||||||
|
this PolicyStatus policy, IOrganizationHasVerifiedDomainsQuery hasVerifiedDomainsQuery)
|
||||||
{
|
{
|
||||||
if (policy.Type is not PolicyType.SingleOrg)
|
if (policy.Type is not PolicyType.SingleOrg)
|
||||||
{
|
{
|
||||||
throw new ArgumentException($"'{nameof(policy)}' must be of type '{nameof(PolicyType.SingleOrg)}'.", nameof(policy));
|
throw new ArgumentException($"'{nameof(policy)}' must be of type '{nameof(PolicyType.SingleOrg)}'.", nameof(policy));
|
||||||
}
|
}
|
||||||
return new PolicyDetailResponseModel(policy, await CanToggleState());
|
|
||||||
|
return new PolicyStatusResponseModel(policy, await CanToggleState());
|
||||||
|
|
||||||
async Task<bool> CanToggleState()
|
async Task<bool> CanToggleState()
|
||||||
{
|
{
|
||||||
@ -25,5 +27,4 @@ public static class PolicyDetailResponses
|
|||||||
return !policy.Enabled;
|
return !policy.Enabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,20 +0,0 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
|
|
||||||
|
|
||||||
public class PolicyDetailResponseModel : PolicyResponseModel
|
|
||||||
{
|
|
||||||
public PolicyDetailResponseModel(Policy policy, string obj = "policy") : base(policy, obj)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public PolicyDetailResponseModel(Policy policy, bool canToggleState) : base(policy)
|
|
||||||
{
|
|
||||||
CanToggleState = canToggleState;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates whether the Policy can be enabled/disabled
|
|
||||||
/// </summary>
|
|
||||||
public bool CanToggleState { get; set; } = true;
|
|
||||||
}
|
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.Models.Api;
|
||||||
|
|
||||||
|
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
|
|
||||||
|
public class PolicyStatusResponseModel : ResponseModel
|
||||||
|
{
|
||||||
|
public PolicyStatusResponseModel(PolicyStatus policy, bool canToggleState = true) : base("policy")
|
||||||
|
{
|
||||||
|
OrganizationId = policy.OrganizationId;
|
||||||
|
Type = policy.Type;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(policy.Data))
|
||||||
|
{
|
||||||
|
Data = JsonSerializer.Deserialize<Dictionary<string, object>>(policy.Data) ?? new();
|
||||||
|
}
|
||||||
|
|
||||||
|
Enabled = policy.Enabled;
|
||||||
|
CanToggleState = canToggleState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Guid OrganizationId { get; init; }
|
||||||
|
public PolicyType Type { get; init; }
|
||||||
|
public Dictionary<string, object> Data { get; init; } = new();
|
||||||
|
public bool Enabled { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether the Policy can be enabled/disabled
|
||||||
|
/// </summary>
|
||||||
|
public bool CanToggleState { get; init; }
|
||||||
|
}
|
||||||
@ -1,4 +1,6 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
<Sdk Name="Bitwarden.Server.Sdk" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<UserSecretsId>bitwarden-Api</UserSecretsId>
|
<UserSecretsId>bitwarden-Api</UserSecretsId>
|
||||||
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||||
|
|||||||
@ -6,7 +6,7 @@ using Bit.Api.Models.Response;
|
|||||||
using Bit.Api.Models.Response.Organizations;
|
using Bit.Api.Models.Response.Organizations;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -38,7 +38,7 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
private readonly ICloudSyncSponsorshipsCommand _syncSponsorshipsCommand;
|
private readonly ICloudSyncSponsorshipsCommand _syncSponsorshipsCommand;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyQuery _policyQuery;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
public OrganizationSponsorshipsController(
|
public OrganizationSponsorshipsController(
|
||||||
@ -55,7 +55,7 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
ICloudSyncSponsorshipsCommand syncSponsorshipsCommand,
|
ICloudSyncSponsorshipsCommand syncSponsorshipsCommand,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyQuery policyQuery,
|
||||||
IFeatureService featureService)
|
IFeatureService featureService)
|
||||||
{
|
{
|
||||||
_organizationSponsorshipRepository = organizationSponsorshipRepository;
|
_organizationSponsorshipRepository = organizationSponsorshipRepository;
|
||||||
@ -71,7 +71,7 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
_syncSponsorshipsCommand = syncSponsorshipsCommand;
|
_syncSponsorshipsCommand = syncSponsorshipsCommand;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_policyRepository = policyRepository;
|
_policyQuery = policyQuery;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,10 +81,10 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipCreateRequestModel model)
|
public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipCreateRequestModel model)
|
||||||
{
|
{
|
||||||
var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId);
|
var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId);
|
||||||
var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsoringOrgId,
|
var freeFamiliesSponsorshipPolicy = await _policyQuery.RunAsync(sponsoringOrgId,
|
||||||
PolicyType.FreeFamiliesSponsorshipPolicy);
|
PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||||
|
|
||||||
if (freeFamiliesSponsorshipPolicy?.Enabled == true)
|
if (freeFamiliesSponsorshipPolicy.Enabled)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Free Bitwarden Families sponsorship has been disabled by your organization administrator.");
|
throw new BadRequestException("Free Bitwarden Families sponsorship has been disabled by your organization administrator.");
|
||||||
}
|
}
|
||||||
@ -108,10 +108,10 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task ResendSponsorshipOffer(Guid sponsoringOrgId, [FromQuery] string sponsoredFriendlyName)
|
public async Task ResendSponsorshipOffer(Guid sponsoringOrgId, [FromQuery] string sponsoredFriendlyName)
|
||||||
{
|
{
|
||||||
var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsoringOrgId,
|
var freeFamiliesSponsorshipPolicy = await _policyQuery.RunAsync(sponsoringOrgId,
|
||||||
PolicyType.FreeFamiliesSponsorshipPolicy);
|
PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||||
|
|
||||||
if (freeFamiliesSponsorshipPolicy?.Enabled == true)
|
if (freeFamiliesSponsorshipPolicy.Enabled)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Free Bitwarden Families sponsorship has been disabled by your organization administrator.");
|
throw new BadRequestException("Free Bitwarden Families sponsorship has been disabled by your organization administrator.");
|
||||||
}
|
}
|
||||||
@ -138,9 +138,9 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
var (isValid, sponsorship) = await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email);
|
var (isValid, sponsorship) = await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email);
|
||||||
if (isValid && sponsorship.SponsoringOrganizationId.HasValue)
|
if (isValid && sponsorship.SponsoringOrganizationId.HasValue)
|
||||||
{
|
{
|
||||||
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsorship.SponsoringOrganizationId.Value,
|
var policy = await _policyQuery.RunAsync(sponsorship.SponsoringOrganizationId.Value,
|
||||||
PolicyType.FreeFamiliesSponsorshipPolicy);
|
PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||||
isFreeFamilyPolicyEnabled = policy?.Enabled ?? false;
|
isFreeFamilyPolicyEnabled = policy.Enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = PreValidateSponsorshipResponseModel.From(isValid, isFreeFamilyPolicyEnabled);
|
var response = PreValidateSponsorshipResponseModel.From(isValid, isFreeFamilyPolicyEnabled);
|
||||||
@ -165,10 +165,10 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
throw new BadRequestException("Can only redeem sponsorship for an organization you own.");
|
throw new BadRequestException("Can only redeem sponsorship for an organization you own.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(
|
var freeFamiliesSponsorshipPolicy = await _policyQuery.RunAsync(
|
||||||
model.SponsoredOrganizationId, PolicyType.FreeFamiliesSponsorshipPolicy);
|
model.SponsoredOrganizationId, PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||||
|
|
||||||
if (freeFamiliesSponsorshipPolicy?.Enabled == true)
|
if (freeFamiliesSponsorshipPolicy.Enabled)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Free Bitwarden Families sponsorship has been disabled by your organization administrator.");
|
throw new BadRequestException("Free Bitwarden Families sponsorship has been disabled by your organization administrator.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
using Bit.Api.Billing.Attributes;
|
using Bit.Api.Billing.Attributes;
|
||||||
using Bit.Api.Billing.Models.Requests.Tax;
|
using Bit.Api.Billing.Models.Requests.PreviewInvoice;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Billing.Organizations.Commands;
|
using Bit.Core.Billing.Organizations.Commands;
|
||||||
using Bit.Core.Billing.Premium.Commands;
|
using Bit.Core.Billing.Premium.Commands;
|
||||||
|
using Bit.Core.Entities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
@ -10,10 +11,11 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
|
|||||||
namespace Bit.Api.Billing.Controllers;
|
namespace Bit.Api.Billing.Controllers;
|
||||||
|
|
||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
[Route("billing/tax")]
|
[Route("billing/preview-invoice")]
|
||||||
public class TaxController(
|
public class PreviewInvoiceController(
|
||||||
IPreviewOrganizationTaxCommand previewOrganizationTaxCommand,
|
IPreviewOrganizationTaxCommand previewOrganizationTaxCommand,
|
||||||
IPreviewPremiumTaxCommand previewPremiumTaxCommand) : BaseBillingController
|
IPreviewPremiumTaxCommand previewPremiumTaxCommand,
|
||||||
|
IPreviewPremiumUpgradeProrationCommand previewPremiumUpgradeProrationCommand) : BaseBillingController
|
||||||
{
|
{
|
||||||
[HttpPost("organizations/subscriptions/purchase")]
|
[HttpPost("organizations/subscriptions/purchase")]
|
||||||
public async Task<IResult> PreviewOrganizationSubscriptionPurchaseTaxAsync(
|
public async Task<IResult> PreviewOrganizationSubscriptionPurchaseTaxAsync(
|
||||||
@ -21,11 +23,7 @@ public class TaxController(
|
|||||||
{
|
{
|
||||||
var (purchase, billingAddress) = request.ToDomain();
|
var (purchase, billingAddress) = request.ToDomain();
|
||||||
var result = await previewOrganizationTaxCommand.Run(purchase, billingAddress);
|
var result = await previewOrganizationTaxCommand.Run(purchase, billingAddress);
|
||||||
return Handle(result.Map(pair => new
|
return Handle(result.Map(pair => new { pair.Tax, pair.Total }));
|
||||||
{
|
|
||||||
pair.Tax,
|
|
||||||
pair.Total
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("organizations/{organizationId:guid}/subscription/plan-change")]
|
[HttpPost("organizations/{organizationId:guid}/subscription/plan-change")]
|
||||||
@ -36,11 +34,7 @@ public class TaxController(
|
|||||||
{
|
{
|
||||||
var (planChange, billingAddress) = request.ToDomain();
|
var (planChange, billingAddress) = request.ToDomain();
|
||||||
var result = await previewOrganizationTaxCommand.Run(organization, planChange, billingAddress);
|
var result = await previewOrganizationTaxCommand.Run(organization, planChange, billingAddress);
|
||||||
return Handle(result.Map(pair => new
|
return Handle(result.Map(pair => new { pair.Tax, pair.Total }));
|
||||||
{
|
|
||||||
pair.Tax,
|
|
||||||
pair.Total
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("organizations/{organizationId:guid}/subscription/update")]
|
[HttpPut("organizations/{organizationId:guid}/subscription/update")]
|
||||||
@ -51,11 +45,7 @@ public class TaxController(
|
|||||||
{
|
{
|
||||||
var update = request.ToDomain();
|
var update = request.ToDomain();
|
||||||
var result = await previewOrganizationTaxCommand.Run(organization, update);
|
var result = await previewOrganizationTaxCommand.Run(organization, update);
|
||||||
return Handle(result.Map(pair => new
|
return Handle(result.Map(pair => new { pair.Tax, pair.Total }));
|
||||||
{
|
|
||||||
pair.Tax,
|
|
||||||
pair.Total
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("premium/subscriptions/purchase")]
|
[HttpPost("premium/subscriptions/purchase")]
|
||||||
@ -64,10 +54,29 @@ public class TaxController(
|
|||||||
{
|
{
|
||||||
var (purchase, billingAddress) = request.ToDomain();
|
var (purchase, billingAddress) = request.ToDomain();
|
||||||
var result = await previewPremiumTaxCommand.Run(purchase, billingAddress);
|
var result = await previewPremiumTaxCommand.Run(purchase, billingAddress);
|
||||||
return Handle(result.Map(pair => new
|
return Handle(result.Map(pair => new { pair.Tax, pair.Total }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("premium/subscriptions/upgrade")]
|
||||||
|
[InjectUser]
|
||||||
|
public async Task<IResult> PreviewPremiumUpgradeProrationAsync(
|
||||||
|
[BindNever] User user,
|
||||||
|
[FromBody] PreviewPremiumUpgradeProrationRequest request)
|
||||||
|
{
|
||||||
|
var (planType, billingAddress) = request.ToDomain();
|
||||||
|
|
||||||
|
var result = await previewPremiumUpgradeProrationCommand.Run(
|
||||||
|
user,
|
||||||
|
planType,
|
||||||
|
billingAddress);
|
||||||
|
|
||||||
|
return Handle(result.Map(proration => new
|
||||||
{
|
{
|
||||||
pair.Tax,
|
proration.NewPlanProratedAmount,
|
||||||
pair.Total
|
proration.Credit,
|
||||||
|
proration.Tax,
|
||||||
|
proration.Total,
|
||||||
|
proration.NewPlanProratedMonths
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,8 +132,8 @@ public class AccountBillingVNextController(
|
|||||||
[BindNever] User user,
|
[BindNever] User user,
|
||||||
[FromBody] UpgradePremiumToOrganizationRequest request)
|
[FromBody] UpgradePremiumToOrganizationRequest request)
|
||||||
{
|
{
|
||||||
var (organizationName, key, planType) = request.ToDomain();
|
var (organizationName, key, planType, billingAddress) = request.ToDomain();
|
||||||
var result = await upgradePremiumToOrganizationCommand.Run(user, organizationName, key, planType);
|
var result = await upgradePremiumToOrganizationCommand.Run(user, organizationName, key, planType, billingAddress);
|
||||||
return Handle(result);
|
return Handle(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Api.Billing.Models.Requests.Payment;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Models.Requests.Premium;
|
namespace Bit.Api.Billing.Models.Requests.Premium;
|
||||||
@ -14,24 +15,30 @@ public class UpgradePremiumToOrganizationRequest
|
|||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
public ProductTierType Tier { get; set; }
|
public required ProductTierType TargetProductTierType { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
public required MinimalBillingAddressRequest BillingAddress { get; set; }
|
||||||
public PlanCadenceType Cadence { get; set; }
|
|
||||||
|
|
||||||
private PlanType PlanType =>
|
private PlanType PlanType
|
||||||
Tier switch
|
{
|
||||||
|
get
|
||||||
{
|
{
|
||||||
ProductTierType.Families => PlanType.FamiliesAnnually,
|
if (TargetProductTierType is not (ProductTierType.Families or ProductTierType.Teams or ProductTierType.Enterprise))
|
||||||
ProductTierType.Teams => Cadence == PlanCadenceType.Monthly
|
{
|
||||||
? PlanType.TeamsMonthly
|
throw new InvalidOperationException($"Cannot upgrade Premium subscription to {TargetProductTierType} plan.");
|
||||||
: PlanType.TeamsAnnually,
|
}
|
||||||
ProductTierType.Enterprise => Cadence == PlanCadenceType.Monthly
|
|
||||||
? PlanType.EnterpriseMonthly
|
|
||||||
: PlanType.EnterpriseAnnually,
|
|
||||||
_ => throw new InvalidOperationException("Cannot upgrade to an Organization subscription that isn't Families, Teams or Enterprise.")
|
|
||||||
};
|
|
||||||
|
|
||||||
public (string OrganizationName, string Key, PlanType PlanType) ToDomain() => (OrganizationName, Key, PlanType);
|
return TargetProductTierType switch
|
||||||
|
{
|
||||||
|
ProductTierType.Families => PlanType.FamiliesAnnually,
|
||||||
|
ProductTierType.Teams => PlanType.TeamsAnnually,
|
||||||
|
ProductTierType.Enterprise => PlanType.EnterpriseAnnually,
|
||||||
|
_ => throw new InvalidOperationException($"Unexpected ProductTierType: {TargetProductTierType}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public (string OrganizationName, string Key, PlanType PlanType, Core.Billing.Payment.Models.BillingAddress BillingAddress) ToDomain() =>
|
||||||
|
(OrganizationName, Key, PlanType, BillingAddress.ToDomain());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ using Bit.Api.Billing.Models.Requests.Payment;
|
|||||||
using Bit.Core.Billing.Organizations.Models;
|
using Bit.Core.Billing.Organizations.Models;
|
||||||
using Bit.Core.Billing.Payment.Models;
|
using Bit.Core.Billing.Payment.Models;
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Models.Requests.Tax;
|
namespace Bit.Api.Billing.Models.Requests.PreviewInvoice;
|
||||||
|
|
||||||
public record PreviewOrganizationSubscriptionPlanChangeTaxRequest
|
public record PreviewOrganizationSubscriptionPlanChangeTaxRequest
|
||||||
{
|
{
|
||||||
@ -4,7 +4,7 @@ using Bit.Api.Billing.Models.Requests.Payment;
|
|||||||
using Bit.Core.Billing.Organizations.Models;
|
using Bit.Core.Billing.Organizations.Models;
|
||||||
using Bit.Core.Billing.Payment.Models;
|
using Bit.Core.Billing.Payment.Models;
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Models.Requests.Tax;
|
namespace Bit.Api.Billing.Models.Requests.PreviewInvoice;
|
||||||
|
|
||||||
public record PreviewOrganizationSubscriptionPurchaseTaxRequest
|
public record PreviewOrganizationSubscriptionPurchaseTaxRequest
|
||||||
{
|
{
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using Bit.Api.Billing.Models.Requests.Organizations;
|
using Bit.Api.Billing.Models.Requests.Organizations;
|
||||||
using Bit.Core.Billing.Organizations.Models;
|
using Bit.Core.Billing.Organizations.Models;
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Models.Requests.Tax;
|
namespace Bit.Api.Billing.Models.Requests.PreviewInvoice;
|
||||||
|
|
||||||
public class PreviewOrganizationSubscriptionUpdateTaxRequest
|
public class PreviewOrganizationSubscriptionUpdateTaxRequest
|
||||||
{
|
{
|
||||||
@ -2,7 +2,7 @@
|
|||||||
using Bit.Api.Billing.Models.Requests.Payment;
|
using Bit.Api.Billing.Models.Requests.Payment;
|
||||||
using Bit.Core.Billing.Payment.Models;
|
using Bit.Core.Billing.Payment.Models;
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Models.Requests.Tax;
|
namespace Bit.Api.Billing.Models.Requests.PreviewInvoice;
|
||||||
|
|
||||||
public record PreviewPremiumSubscriptionPurchaseTaxRequest
|
public record PreviewPremiumSubscriptionPurchaseTaxRequest
|
||||||
{
|
{
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Api.Billing.Models.Requests.Payment;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Payment.Models;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing.Models.Requests.PreviewInvoice;
|
||||||
|
|
||||||
|
public record PreviewPremiumUpgradeProrationRequest
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public required ProductTierType TargetProductTierType { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public required MinimalBillingAddressRequest BillingAddress { get; set; }
|
||||||
|
|
||||||
|
private PlanType PlanType
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (TargetProductTierType is not (ProductTierType.Families or ProductTierType.Teams or ProductTierType.Enterprise))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Cannot upgrade Premium subscription to {TargetProductTierType} plan.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return TargetProductTierType switch
|
||||||
|
{
|
||||||
|
ProductTierType.Families => PlanType.FamiliesAnnually,
|
||||||
|
ProductTierType.Teams => PlanType.TeamsAnnually,
|
||||||
|
ProductTierType.Enterprise => PlanType.EnterpriseAnnually,
|
||||||
|
_ => throw new InvalidOperationException($"Unexpected ProductTierType: {TargetProductTierType}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public (PlanType, BillingAddress) ToDomain() =>
|
||||||
|
(PlanType, BillingAddress.ToDomain());
|
||||||
|
}
|
||||||
@ -8,7 +8,7 @@ public class Program
|
|||||||
{
|
{
|
||||||
Host
|
Host
|
||||||
.CreateDefaultBuilder(args)
|
.CreateDefaultBuilder(args)
|
||||||
.ConfigureCustomAppConfiguration(args)
|
.UseBitwardenSdk()
|
||||||
.ConfigureWebHostDefaults(webBuilder =>
|
.ConfigureWebHostDefaults(webBuilder =>
|
||||||
{
|
{
|
||||||
webBuilder.UseStartup<Startup>();
|
webBuilder.UseStartup<Startup>();
|
||||||
|
|||||||
@ -14,7 +14,6 @@ using Bit.Api.Tools.Models.Request;
|
|||||||
using Bit.Api.Vault.Models.Request;
|
using Bit.Api.Vault.Models.Request;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.SharedWeb.Health;
|
using Bit.SharedWeb.Health;
|
||||||
using Microsoft.IdentityModel.Logging;
|
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using Bit.SharedWeb.Utilities;
|
using Bit.SharedWeb.Utilities;
|
||||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||||
@ -238,8 +237,6 @@ public class Startup
|
|||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
ILogger<Startup> logger)
|
ILogger<Startup> logger)
|
||||||
{
|
{
|
||||||
IdentityModelEventSource.ShowPII = true;
|
|
||||||
|
|
||||||
// Add general security headers
|
// Add general security headers
|
||||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||||
|
|
||||||
|
|||||||
@ -74,11 +74,6 @@ public class ImportCiphersController : Controller
|
|||||||
throw new BadRequestException("You cannot import this much data at once.");
|
throw new BadRequestException("You cannot import this much data at once.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.Ciphers.Any(c => c.ArchivedDate.HasValue))
|
|
||||||
{
|
|
||||||
throw new BadRequestException("You cannot import archived items into an organization.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var orgId = new Guid(organizationId);
|
var orgId = new Guid(organizationId);
|
||||||
var collections = model.Collections.Select(c => c.ToCollection(orgId)).ToList();
|
var collections = model.Collections.Select(c => c.ToCollection(orgId)).ToList();
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
|
||||||
|
public class PolicyStatus
|
||||||
|
{
|
||||||
|
public PolicyStatus(Guid organizationId, PolicyType policyType, Policy? policy = null)
|
||||||
|
{
|
||||||
|
OrganizationId = policy?.OrganizationId ?? organizationId;
|
||||||
|
Data = policy?.Data;
|
||||||
|
Type = policy?.Type ?? policyType;
|
||||||
|
Enabled = policy?.Enabled ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
public PolicyType Type { get; set; }
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
public string? Data { get; set; }
|
||||||
|
|
||||||
|
public T GetDataModel<T>() where T : IPolicyDataModel, new()
|
||||||
|
{
|
||||||
|
return CoreHelpers.LoadClassFromJsonData<T>(Data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Identity;
|
|||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
|
||||||
|
|
||||||
public class AdminRecoverAccountCommand(IOrganizationRepository organizationRepository,
|
public class AdminRecoverAccountCommand(IOrganizationRepository organizationRepository,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyQuery policyQuery,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
@ -30,9 +30,8 @@ public class AdminRecoverAccountCommand(IOrganizationRepository organizationRepo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enterprise policy must be enabled
|
// Enterprise policy must be enabled
|
||||||
var resetPasswordPolicy =
|
var resetPasswordPolicy = await policyQuery.RunAsync(orgId, PolicyType.ResetPassword);
|
||||||
await policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
if (!resetPasswordPolicy.Enabled)
|
||||||
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled)
|
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Organization does not have the password reset policy enabled.");
|
throw new BadRequestException("Organization does not have the password reset policy enabled.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimed
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
|
||||||
using Bit.Core.AdminConsole.Utilities.v2;
|
using Bit.Core.AdminConsole.Utilities.v2;
|
||||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
@ -20,7 +19,7 @@ public class AutomaticallyConfirmOrganizationUsersValidator(
|
|||||||
IPolicyRequirementQuery policyRequirementQuery,
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator,
|
IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IPolicyRepository policyRepository) : IAutomaticallyConfirmOrganizationUsersValidator
|
IPolicyQuery policyQuery) : IAutomaticallyConfirmOrganizationUsersValidator
|
||||||
{
|
{
|
||||||
public async Task<ValidationResult<AutomaticallyConfirmOrganizationUserValidationRequest>> ValidateAsync(
|
public async Task<ValidationResult<AutomaticallyConfirmOrganizationUserValidationRequest>> ValidateAsync(
|
||||||
AutomaticallyConfirmOrganizationUserValidationRequest request)
|
AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||||
@ -74,7 +73,7 @@ public class AutomaticallyConfirmOrganizationUsersValidator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(AutomaticallyConfirmOrganizationUserValidationRequest request) =>
|
private async Task<bool> OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(AutomaticallyConfirmOrganizationUserValidationRequest request) =>
|
||||||
await policyRepository.GetByOrganizationIdTypeAsync(request.OrganizationId, PolicyType.AutomaticUserConfirmation) is { Enabled: true }
|
(await policyQuery.RunAsync(request.OrganizationId, PolicyType.AutomaticUserConfirmation)).Enabled
|
||||||
&& request.Organization is { UseAutomaticUserConfirmation: true };
|
&& request.Organization is { UseAutomaticUserConfirmation: true };
|
||||||
|
|
||||||
private async Task<bool> OrganizationUserConformsToTwoFactorRequiredPolicyAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
private async Task<bool> OrganizationUserConformsToTwoFactorRequiredPolicyAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.Auth.Models.Business;
|
using Bit.Core.Auth.Models.Business;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
@ -19,7 +19,7 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUse
|
|||||||
public class SendOrganizationInvitesCommand(
|
public class SendOrganizationInvitesCommand(
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
ISsoConfigRepository ssoConfigurationRepository,
|
ISsoConfigRepository ssoConfigurationRepository,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyQuery policyQuery,
|
||||||
IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory,
|
IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory,
|
||||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> dataProtectorTokenFactory,
|
IDataProtectorTokenFactory<OrgUserInviteTokenable> dataProtectorTokenFactory,
|
||||||
IMailService mailService) : ISendOrganizationInvitesCommand
|
IMailService mailService) : ISendOrganizationInvitesCommand
|
||||||
@ -58,7 +58,7 @@ public class SendOrganizationInvitesCommand(
|
|||||||
// need to check the policy if the org has SSO enabled.
|
// need to check the policy if the org has SSO enabled.
|
||||||
var orgSsoLoginRequiredPolicyEnabled = orgSsoEnabled &&
|
var orgSsoLoginRequiredPolicyEnabled = orgSsoEnabled &&
|
||||||
organization.UsePolicies &&
|
organization.UsePolicies &&
|
||||||
(await policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.RequireSso))?.Enabled == true;
|
(await policyQuery.RunAsync(organization.Id, PolicyType.RequireSso)).Enabled;
|
||||||
|
|
||||||
// Generate the list of org users and expiring tokens
|
// Generate the list of org users and expiring tokens
|
||||||
// create helper function to create expiring tokens
|
// create helper function to create expiring tokens
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
|
|
||||||
|
public interface IPolicyQuery
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a summary view of an organization's usage of a policy specified by the <paramref name="policyType"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This query is the entrypoint for consumers interested in understanding how a particular <see cref="PolicyType"/>
|
||||||
|
/// has been applied to an organization; the resultant <see cref="PolicyStatus"/> is not indicative of explicit
|
||||||
|
/// policy configuration.
|
||||||
|
/// </remarks>
|
||||||
|
Task<PolicyStatus> RunAsync(Guid organizationId, PolicyType policyType);
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||||
|
|
||||||
|
public class PolicyQuery(IPolicyRepository policyRepository) : IPolicyQuery
|
||||||
|
{
|
||||||
|
public async Task<PolicyStatus> RunAsync(Guid organizationId, PolicyType policyType)
|
||||||
|
{
|
||||||
|
var dbPolicy = await policyRepository.GetByOrganizationIdTypeAsync(organizationId, policyType);
|
||||||
|
return new PolicyStatus(organizationId, policyType, dbPolicy);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,6 +18,7 @@ public static class PolicyServiceCollectionExtensions
|
|||||||
services.AddScoped<ISavePolicyCommand, SavePolicyCommand>();
|
services.AddScoped<ISavePolicyCommand, SavePolicyCommand>();
|
||||||
services.AddScoped<IVNextSavePolicyCommand, VNextSavePolicyCommand>();
|
services.AddScoped<IVNextSavePolicyCommand, VNextSavePolicyCommand>();
|
||||||
services.AddScoped<IPolicyRequirementQuery, PolicyRequirementQuery>();
|
services.AddScoped<IPolicyRequirementQuery, PolicyRequirementQuery>();
|
||||||
|
services.AddScoped<IPolicyQuery, PolicyQuery>();
|
||||||
services.AddScoped<IPolicyEventHandlerFactory, PolicyEventHandlerHandlerFactory>();
|
services.AddScoped<IPolicyEventHandlerFactory, PolicyEventHandlerHandlerFactory>();
|
||||||
|
|
||||||
services.AddPolicyValidators();
|
services.AddPolicyValidators();
|
||||||
|
|||||||
@ -48,7 +48,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private readonly IStripePaymentService _paymentService;
|
private readonly IStripePaymentService _paymentService;
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyQuery _policyQuery;
|
||||||
private readonly IPolicyService _policyService;
|
private readonly IPolicyService _policyService;
|
||||||
private readonly ISsoUserRepository _ssoUserRepository;
|
private readonly ISsoUserRepository _ssoUserRepository;
|
||||||
private readonly IGlobalSettings _globalSettings;
|
private readonly IGlobalSettings _globalSettings;
|
||||||
@ -75,7 +75,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
IStripePaymentService paymentService,
|
IStripePaymentService paymentService,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyQuery policyQuery,
|
||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
ISsoUserRepository ssoUserRepository,
|
ISsoUserRepository ssoUserRepository,
|
||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
@ -102,7 +102,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_paymentService = paymentService;
|
_paymentService = paymentService;
|
||||||
_policyRepository = policyRepository;
|
_policyQuery = policyQuery;
|
||||||
_policyService = policyService;
|
_policyService = policyService;
|
||||||
_ssoUserRepository = ssoUserRepository;
|
_ssoUserRepository = ssoUserRepository;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
@ -835,9 +835,8 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the organization has the policy enabled
|
// Make sure the organization has the policy enabled
|
||||||
var resetPasswordPolicy =
|
var resetPasswordPolicy = await _policyQuery.RunAsync(organizationId, PolicyType.ResetPassword);
|
||||||
await _policyRepository.GetByOrganizationIdTypeAsync(organizationId, PolicyType.ResetPassword);
|
if (!resetPasswordPolicy.Enabled)
|
||||||
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled)
|
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Organization does not have the password reset policy enabled.");
|
throw new BadRequestException("Organization does not have the password reset policy enabled.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -147,16 +147,12 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
|
|||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Support up to 5 keys
|
// Load all WebAuthn credentials stored in metadata. The number of allowed credentials
|
||||||
for (var i = 1; i <= 5; i++)
|
// is controlled by credential registration.
|
||||||
|
foreach (var kvp in provider.MetaData.Where(k => k.Key.StartsWith("Key")))
|
||||||
{
|
{
|
||||||
var keyName = $"Key{i}";
|
var key = new TwoFactorProvider.WebAuthnData((dynamic)kvp.Value);
|
||||||
if (provider.MetaData.TryGetValue(keyName, out var value))
|
keys.Add(new Tuple<string, TwoFactorProvider.WebAuthnData>(kvp.Key, key));
|
||||||
{
|
|
||||||
var key = new TwoFactorProvider.WebAuthnData((dynamic)value);
|
|
||||||
|
|
||||||
keys.Add(new Tuple<string, TwoFactorProvider.WebAuthnData>(keyName, key));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return keys;
|
return keys;
|
||||||
|
|||||||
@ -5,9 +5,9 @@ using Bit.Core.AdminConsole.Entities;
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Models.Data;
|
using Bit.Core.AdminConsole.Models.Data;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||||
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.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
@ -21,7 +21,7 @@ namespace Bit.Core.Auth.Services;
|
|||||||
public class SsoConfigService : ISsoConfigService
|
public class SsoConfigService : ISsoConfigService
|
||||||
{
|
{
|
||||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyQuery _policyQuery;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
@ -29,14 +29,14 @@ public class SsoConfigService : ISsoConfigService
|
|||||||
|
|
||||||
public SsoConfigService(
|
public SsoConfigService(
|
||||||
ISsoConfigRepository ssoConfigRepository,
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyQuery policyQuery,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IVNextSavePolicyCommand vNextSavePolicyCommand)
|
IVNextSavePolicyCommand vNextSavePolicyCommand)
|
||||||
{
|
{
|
||||||
_ssoConfigRepository = ssoConfigRepository;
|
_ssoConfigRepository = ssoConfigRepository;
|
||||||
_policyRepository = policyRepository;
|
_policyQuery = policyQuery;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
@ -114,14 +114,14 @@ public class SsoConfigService : ISsoConfigService
|
|||||||
throw new BadRequestException("Organization cannot use Key Connector.");
|
throw new BadRequestException("Organization cannot use Key Connector.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var singleOrgPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.SingleOrg);
|
var singleOrgPolicy = await _policyQuery.RunAsync(config.OrganizationId, PolicyType.SingleOrg);
|
||||||
if (singleOrgPolicy is not { Enabled: true })
|
if (!singleOrgPolicy.Enabled)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Key Connector requires the Single Organization policy to be enabled.");
|
throw new BadRequestException("Key Connector requires the Single Organization policy to be enabled.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var ssoPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.RequireSso);
|
var ssoPolicy = await _policyQuery.RunAsync(config.OrganizationId, PolicyType.RequireSso);
|
||||||
if (ssoPolicy is not { Enabled: true })
|
if (!ssoPolicy.Enabled)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Key Connector requires the Single Sign-On Authentication policy to be enabled.");
|
throw new BadRequestException("Key Connector requires the Single Sign-On Authentication policy to be enabled.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
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.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Models;
|
using Bit.Core.Auth.Models;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
@ -27,7 +27,7 @@ public class RegisterUserCommand : IRegisterUserCommand
|
|||||||
private readonly IGlobalSettings _globalSettings;
|
private readonly IGlobalSettings _globalSettings;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyQuery _policyQuery;
|
||||||
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ public class RegisterUserCommand : IRegisterUserCommand
|
|||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyQuery policyQuery,
|
||||||
IOrganizationDomainRepository organizationDomainRepository,
|
IOrganizationDomainRepository organizationDomainRepository,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IDataProtectionProvider dataProtectionProvider,
|
IDataProtectionProvider dataProtectionProvider,
|
||||||
@ -65,7 +65,7 @@ public class RegisterUserCommand : IRegisterUserCommand
|
|||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_policyRepository = policyRepository;
|
_policyQuery = policyQuery;
|
||||||
_organizationDomainRepository = organizationDomainRepository;
|
_organizationDomainRepository = organizationDomainRepository;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
|
|
||||||
@ -246,9 +246,9 @@ public class RegisterUserCommand : IRegisterUserCommand
|
|||||||
var orgUser = await _organizationUserRepository.GetByIdAsync(orgUserId.Value);
|
var orgUser = await _organizationUserRepository.GetByIdAsync(orgUserId.Value);
|
||||||
if (orgUser != null)
|
if (orgUser != null)
|
||||||
{
|
{
|
||||||
var twoFactorPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(orgUser.OrganizationId,
|
var twoFactorPolicy = await _policyQuery.RunAsync(orgUser.OrganizationId,
|
||||||
PolicyType.TwoFactorAuthentication);
|
PolicyType.TwoFactorAuthentication);
|
||||||
if (twoFactorPolicy != null && twoFactorPolicy.Enabled)
|
if (twoFactorPolicy.Enabled)
|
||||||
{
|
{
|
||||||
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||||
{
|
{
|
||||||
|
|||||||
@ -70,10 +70,6 @@ public static class StripeConstants
|
|||||||
public const string InvoiceApproved = "invoice_approved";
|
public const string InvoiceApproved = "invoice_approved";
|
||||||
public const string OrganizationId = "organizationId";
|
public const string OrganizationId = "organizationId";
|
||||||
public const string PayPalTransactionId = "btPayPalTransactionId";
|
public const string PayPalTransactionId = "btPayPalTransactionId";
|
||||||
public const string PreviousAdditionalStorage = "previous_additional_storage";
|
|
||||||
public const string PreviousPeriodEndDate = "previous_period_end_date";
|
|
||||||
public const string PreviousPremiumPriceId = "previous_premium_price_id";
|
|
||||||
public const string PreviousPremiumUserId = "previous_premium_user_id";
|
|
||||||
public const string ProviderId = "providerId";
|
public const string ProviderId = "providerId";
|
||||||
public const string Region = "region";
|
public const string Region = "region";
|
||||||
public const string RetiredBraintreeCustomerId = "btCustomerId_old";
|
public const string RetiredBraintreeCustomerId = "btCustomerId_old";
|
||||||
|
|||||||
@ -61,6 +61,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<ICreatePremiumCloudHostedSubscriptionCommand, CreatePremiumCloudHostedSubscriptionCommand>();
|
services.AddScoped<ICreatePremiumCloudHostedSubscriptionCommand, CreatePremiumCloudHostedSubscriptionCommand>();
|
||||||
services.AddScoped<ICreatePremiumSelfHostedSubscriptionCommand, CreatePremiumSelfHostedSubscriptionCommand>();
|
services.AddScoped<ICreatePremiumSelfHostedSubscriptionCommand, CreatePremiumSelfHostedSubscriptionCommand>();
|
||||||
services.AddTransient<IPreviewPremiumTaxCommand, PreviewPremiumTaxCommand>();
|
services.AddTransient<IPreviewPremiumTaxCommand, PreviewPremiumTaxCommand>();
|
||||||
|
services.AddScoped<IPreviewPremiumUpgradeProrationCommand, PreviewPremiumUpgradeProrationCommand>();
|
||||||
services.AddScoped<IUpdatePremiumStorageCommand, UpdatePremiumStorageCommand>();
|
services.AddScoped<IUpdatePremiumStorageCommand, UpdatePremiumStorageCommand>();
|
||||||
services.AddScoped<IUpgradePremiumToOrganizationCommand, UpgradePremiumToOrganizationCommand>();
|
services.AddScoped<IUpgradePremiumToOrganizationCommand, UpgradePremiumToOrganizationCommand>();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,166 @@
|
|||||||
|
using Bit.Core.Billing.Commands;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Payment.Models;
|
||||||
|
using Bit.Core.Billing.Premium.Models;
|
||||||
|
using Bit.Core.Billing.Pricing;
|
||||||
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Premium.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Previews the proration details for upgrading a Premium user subscription to an Organization
|
||||||
|
/// plan by using the Stripe API to create an invoice preview, prorated, for the upgrade.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPreviewPremiumUpgradeProrationCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the tax, total cost, and proration credit for upgrading a Premium subscription to an Organization plan.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user with an active Premium subscription.</param>
|
||||||
|
/// <param name="targetPlanType">The target organization plan type.</param>
|
||||||
|
/// <param name="billingAddress">The billing address for tax calculation.</param>
|
||||||
|
/// <returns>The proration details for the upgrade including costs, credits, tax, and time remaining.</returns>
|
||||||
|
Task<BillingCommandResult<PremiumUpgradeProration>> Run(
|
||||||
|
User user,
|
||||||
|
PlanType targetPlanType,
|
||||||
|
BillingAddress billingAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PreviewPremiumUpgradeProrationCommand(
|
||||||
|
ILogger<PreviewPremiumUpgradeProrationCommand> logger,
|
||||||
|
IPricingClient pricingClient,
|
||||||
|
IStripeAdapter stripeAdapter)
|
||||||
|
: BaseBillingCommand<PreviewPremiumUpgradeProrationCommand>(logger),
|
||||||
|
IPreviewPremiumUpgradeProrationCommand
|
||||||
|
{
|
||||||
|
public Task<BillingCommandResult<PremiumUpgradeProration>> Run(
|
||||||
|
User user,
|
||||||
|
PlanType targetPlanType,
|
||||||
|
BillingAddress billingAddress) => HandleAsync<PremiumUpgradeProration>(async () =>
|
||||||
|
{
|
||||||
|
if (user is not { Premium: true, GatewaySubscriptionId: not null and not "" })
|
||||||
|
{
|
||||||
|
return new BadRequest("User does not have an active Premium subscription.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentSubscription = await stripeAdapter.GetSubscriptionAsync(
|
||||||
|
user.GatewaySubscriptionId,
|
||||||
|
new SubscriptionGetOptions { Expand = ["customer"] });
|
||||||
|
var premiumPlans = await pricingClient.ListPremiumPlans();
|
||||||
|
var passwordManagerItem = currentSubscription.Items.Data.FirstOrDefault(i =>
|
||||||
|
premiumPlans.Any(p => p.Seat.StripePriceId == i.Price.Id));
|
||||||
|
|
||||||
|
if (passwordManagerItem == null)
|
||||||
|
{
|
||||||
|
return new BadRequest("Premium subscription password manager item not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var usersPremiumPlan = premiumPlans.First(p => p.Seat.StripePriceId == passwordManagerItem.Price.Id);
|
||||||
|
var targetPlan = await pricingClient.GetPlanOrThrow(targetPlanType);
|
||||||
|
var subscriptionItems = new List<InvoiceSubscriptionDetailsItemOptions>();
|
||||||
|
var storageItem = currentSubscription.Items.Data.FirstOrDefault(i =>
|
||||||
|
i.Price.Id == usersPremiumPlan.Storage.StripePriceId);
|
||||||
|
|
||||||
|
// Delete the storage item if it exists for this user's plan
|
||||||
|
if (storageItem != null)
|
||||||
|
{
|
||||||
|
subscriptionItems.Add(new InvoiceSubscriptionDetailsItemOptions
|
||||||
|
{
|
||||||
|
Id = storageItem.Id,
|
||||||
|
Deleted = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hardcode seats to 1 for upgrade flow
|
||||||
|
if (targetPlan.HasNonSeatBasedPasswordManagerPlan())
|
||||||
|
{
|
||||||
|
subscriptionItems.Add(new InvoiceSubscriptionDetailsItemOptions
|
||||||
|
{
|
||||||
|
Id = passwordManagerItem.Id,
|
||||||
|
Price = targetPlan.PasswordManager.StripePlanId,
|
||||||
|
Quantity = 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
subscriptionItems.Add(new InvoiceSubscriptionDetailsItemOptions
|
||||||
|
{
|
||||||
|
Id = passwordManagerItem.Id,
|
||||||
|
Price = targetPlan.PasswordManager.StripeSeatPlanId,
|
||||||
|
Quantity = 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = new InvoiceCreatePreviewOptions
|
||||||
|
{
|
||||||
|
AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true },
|
||||||
|
Customer = user.GatewayCustomerId,
|
||||||
|
Subscription = user.GatewaySubscriptionId,
|
||||||
|
CustomerDetails = new InvoiceCustomerDetailsOptions
|
||||||
|
{
|
||||||
|
Address = new AddressOptions
|
||||||
|
{
|
||||||
|
Country = billingAddress.Country,
|
||||||
|
PostalCode = billingAddress.PostalCode
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SubscriptionDetails = new InvoiceSubscriptionDetailsOptions
|
||||||
|
{
|
||||||
|
Items = subscriptionItems,
|
||||||
|
ProrationBehavior = StripeConstants.ProrationBehavior.AlwaysInvoice
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var invoicePreview = await stripeAdapter.CreateInvoicePreviewAsync(options);
|
||||||
|
var proration = GetProration(invoicePreview, passwordManagerItem);
|
||||||
|
|
||||||
|
return proration;
|
||||||
|
});
|
||||||
|
|
||||||
|
private static PremiumUpgradeProration GetProration(Invoice invoicePreview, SubscriptionItem passwordManagerItem) => new()
|
||||||
|
{
|
||||||
|
NewPlanProratedAmount = GetNewPlanProratedAmountFromInvoice(invoicePreview),
|
||||||
|
Credit = GetProrationCreditFromInvoice(invoicePreview),
|
||||||
|
Tax = Convert.ToDecimal(invoicePreview.TotalTaxes.Sum(invoiceTotalTax => invoiceTotalTax.Amount)) / 100,
|
||||||
|
Total = Convert.ToDecimal(invoicePreview.Total) / 100,
|
||||||
|
// Use invoice periodEnd here instead of UtcNow so that testing with Stripe time clocks works correctly. And if there is no test clock,
|
||||||
|
// (like in production), the previewInvoice's periodEnd is the same as UtcNow anyway because of the proration behavior (always_invoice)
|
||||||
|
NewPlanProratedMonths = CalculateNewPlanProratedMonths(invoicePreview.PeriodEnd, passwordManagerItem.CurrentPeriodEnd)
|
||||||
|
};
|
||||||
|
|
||||||
|
private static decimal GetProrationCreditFromInvoice(Invoice invoicePreview)
|
||||||
|
{
|
||||||
|
// Extract proration credit from negative line items (credits are negative in Stripe)
|
||||||
|
var prorationCredit = invoicePreview.Lines?.Data?
|
||||||
|
.Where(line => line.Amount < 0)
|
||||||
|
.Sum(line => Math.Abs(line.Amount)) ?? 0; // Return the credit as positive number
|
||||||
|
|
||||||
|
return Convert.ToDecimal(prorationCredit) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decimal GetNewPlanProratedAmountFromInvoice(Invoice invoicePreview)
|
||||||
|
{
|
||||||
|
// The target plan's prorated upgrade amount should be the only positive-valued line item
|
||||||
|
var proratedTotal = invoicePreview.Lines?.Data?
|
||||||
|
.Where(line => line.Amount > 0)
|
||||||
|
.Sum(line => line.Amount) ?? 0;
|
||||||
|
|
||||||
|
return Convert.ToDecimal(proratedTotal) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CalculateNewPlanProratedMonths(DateTime invoicePeriodEnd, DateTime currentPeriodEnd)
|
||||||
|
{
|
||||||
|
var daysInProratedPeriod = (currentPeriodEnd - invoicePeriodEnd).TotalDays;
|
||||||
|
|
||||||
|
// Round to nearest month (30-day periods)
|
||||||
|
// 1-14 days = 1 month, 15-44 days = 1 month, 45-74 days = 2 months, etc.
|
||||||
|
// Minimum is always 1 month (never returns 0)
|
||||||
|
// Use MidpointRounding.AwayFromZero to round 0.5 up to 1
|
||||||
|
var months = (int)Math.Round(daysInProratedPeriod / 30, MidpointRounding.AwayFromZero);
|
||||||
|
return Math.Max(1, months);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,6 @@
|
|||||||
using Bit.Core.Billing.Commands;
|
using Bit.Core.Billing.Commands;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
@ -28,12 +27,14 @@ public interface IUpgradePremiumToOrganizationCommand
|
|||||||
/// <param name="organizationName">The name for the new organization.</param>
|
/// <param name="organizationName">The name for the new organization.</param>
|
||||||
/// <param name="key">The encrypted organization key for the owner.</param>
|
/// <param name="key">The encrypted organization key for the owner.</param>
|
||||||
/// <param name="targetPlanType">The target organization plan type to upgrade to.</param>
|
/// <param name="targetPlanType">The target organization plan type to upgrade to.</param>
|
||||||
|
/// <param name="billingAddress">The billing address for tax calculation.</param>
|
||||||
/// <returns>A billing command result indicating success or failure with appropriate error details.</returns>
|
/// <returns>A billing command result indicating success or failure with appropriate error details.</returns>
|
||||||
Task<BillingCommandResult<None>> Run(
|
Task<BillingCommandResult<None>> Run(
|
||||||
User user,
|
User user,
|
||||||
string organizationName,
|
string organizationName,
|
||||||
string key,
|
string key,
|
||||||
PlanType targetPlanType);
|
PlanType targetPlanType,
|
||||||
|
Payment.Models.BillingAddress billingAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UpgradePremiumToOrganizationCommand(
|
public class UpgradePremiumToOrganizationCommand(
|
||||||
@ -51,7 +52,8 @@ public class UpgradePremiumToOrganizationCommand(
|
|||||||
User user,
|
User user,
|
||||||
string organizationName,
|
string organizationName,
|
||||||
string key,
|
string key,
|
||||||
PlanType targetPlanType) => HandleAsync<None>(async () =>
|
PlanType targetPlanType,
|
||||||
|
Payment.Models.BillingAddress billingAddress) => HandleAsync<None>(async () =>
|
||||||
{
|
{
|
||||||
// Validate that the user has an active Premium subscription
|
// Validate that the user has an active Premium subscription
|
||||||
if (user is not { Premium: true, GatewaySubscriptionId: not null and not "" })
|
if (user is not { Premium: true, GatewaySubscriptionId: not null and not "" })
|
||||||
@ -74,7 +76,7 @@ public class UpgradePremiumToOrganizationCommand(
|
|||||||
|
|
||||||
if (passwordManagerItem == null)
|
if (passwordManagerItem == null)
|
||||||
{
|
{
|
||||||
return new BadRequest("Premium subscription item not found.");
|
return new BadRequest("Premium subscription password manager item not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var usersPremiumPlan = premiumPlans.First(p => p.Seat.StripePriceId == passwordManagerItem.Price.Id);
|
var usersPremiumPlan = premiumPlans.First(p => p.Seat.StripePriceId == passwordManagerItem.Price.Id);
|
||||||
@ -85,20 +87,10 @@ public class UpgradePremiumToOrganizationCommand(
|
|||||||
// Build the list of subscription item updates
|
// Build the list of subscription item updates
|
||||||
var subscriptionItemOptions = new List<SubscriptionItemOptions>();
|
var subscriptionItemOptions = new List<SubscriptionItemOptions>();
|
||||||
|
|
||||||
// Delete the user's specific password manager item
|
|
||||||
subscriptionItemOptions.Add(new SubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Id = passwordManagerItem.Id,
|
|
||||||
Deleted = true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete the storage item if it exists for this user's plan
|
// Delete the storage item if it exists for this user's plan
|
||||||
var storageItem = currentSubscription.Items.Data.FirstOrDefault(i =>
|
var storageItem = currentSubscription.Items.Data.FirstOrDefault(i =>
|
||||||
i.Price.Id == usersPremiumPlan.Storage.StripePriceId);
|
i.Price.Id == usersPremiumPlan.Storage.StripePriceId);
|
||||||
|
|
||||||
// Capture the previous additional storage quantity for potential revert
|
|
||||||
var previousAdditionalStorage = storageItem?.Quantity ?? 0;
|
|
||||||
|
|
||||||
if (storageItem != null)
|
if (storageItem != null)
|
||||||
{
|
{
|
||||||
subscriptionItemOptions.Add(new SubscriptionItemOptions
|
subscriptionItemOptions.Add(new SubscriptionItemOptions
|
||||||
@ -113,6 +105,7 @@ public class UpgradePremiumToOrganizationCommand(
|
|||||||
{
|
{
|
||||||
subscriptionItemOptions.Add(new SubscriptionItemOptions
|
subscriptionItemOptions.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
|
Id = passwordManagerItem.Id,
|
||||||
Price = targetPlan.PasswordManager.StripePlanId,
|
Price = targetPlan.PasswordManager.StripePlanId,
|
||||||
Quantity = 1
|
Quantity = 1
|
||||||
});
|
});
|
||||||
@ -121,6 +114,7 @@ public class UpgradePremiumToOrganizationCommand(
|
|||||||
{
|
{
|
||||||
subscriptionItemOptions.Add(new SubscriptionItemOptions
|
subscriptionItemOptions.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
|
Id = passwordManagerItem.Id,
|
||||||
Price = targetPlan.PasswordManager.StripeSeatPlanId,
|
Price = targetPlan.PasswordManager.StripeSeatPlanId,
|
||||||
Quantity = seats
|
Quantity = seats
|
||||||
});
|
});
|
||||||
@ -133,14 +127,12 @@ public class UpgradePremiumToOrganizationCommand(
|
|||||||
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
|
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
|
||||||
{
|
{
|
||||||
Items = subscriptionItemOptions,
|
Items = subscriptionItemOptions,
|
||||||
ProrationBehavior = StripeConstants.ProrationBehavior.None,
|
ProrationBehavior = StripeConstants.ProrationBehavior.AlwaysInvoice,
|
||||||
|
BillingCycleAnchor = SubscriptionBillingCycleAnchor.Unchanged,
|
||||||
|
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
|
||||||
Metadata = new Dictionary<string, string>
|
Metadata = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
[StripeConstants.MetadataKeys.OrganizationId] = organizationId.ToString(),
|
[StripeConstants.MetadataKeys.OrganizationId] = organizationId.ToString(),
|
||||||
[StripeConstants.MetadataKeys.PreviousPremiumPriceId] = usersPremiumPlan.Seat.StripePriceId,
|
|
||||||
[StripeConstants.MetadataKeys.PreviousPeriodEndDate] = currentSubscription.GetCurrentPeriodEnd()?.ToString("O") ?? string.Empty,
|
|
||||||
[StripeConstants.MetadataKeys.PreviousAdditionalStorage] = previousAdditionalStorage.ToString(),
|
|
||||||
[StripeConstants.MetadataKeys.PreviousPremiumUserId] = user.Id.ToString(),
|
|
||||||
[StripeConstants.MetadataKeys.UserId] = string.Empty // Remove userId to unlink subscription from User
|
[StripeConstants.MetadataKeys.UserId] = string.Empty // Remove userId to unlink subscription from User
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -152,7 +144,7 @@ public class UpgradePremiumToOrganizationCommand(
|
|||||||
Name = organizationName,
|
Name = organizationName,
|
||||||
BillingEmail = user.Email,
|
BillingEmail = user.Email,
|
||||||
PlanType = targetPlan.Type,
|
PlanType = targetPlan.Type,
|
||||||
Seats = (short)seats,
|
Seats = seats,
|
||||||
MaxCollections = targetPlan.PasswordManager.MaxCollections,
|
MaxCollections = targetPlan.PasswordManager.MaxCollections,
|
||||||
MaxStorageGb = targetPlan.PasswordManager.BaseStorageGb,
|
MaxStorageGb = targetPlan.PasswordManager.BaseStorageGb,
|
||||||
UsePolicies = targetPlan.HasPolicies,
|
UsePolicies = targetPlan.HasPolicies,
|
||||||
@ -182,6 +174,16 @@ public class UpgradePremiumToOrganizationCommand(
|
|||||||
GatewaySubscriptionId = currentSubscription.Id
|
GatewaySubscriptionId = currentSubscription.Id
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Update customer billing address for tax calculation
|
||||||
|
await stripeAdapter.UpdateCustomerAsync(user.GatewayCustomerId, new CustomerUpdateOptions
|
||||||
|
{
|
||||||
|
Address = new AddressOptions
|
||||||
|
{
|
||||||
|
Country = billingAddress.Country,
|
||||||
|
PostalCode = billingAddress.PostalCode
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Update the subscription in Stripe
|
// Update the subscription in Stripe
|
||||||
await stripeAdapter.UpdateSubscriptionAsync(currentSubscription.Id, subscriptionUpdateOptions);
|
await stripeAdapter.UpdateSubscriptionAsync(currentSubscription.Id, subscriptionUpdateOptions);
|
||||||
|
|
||||||
|
|||||||
36
src/Core/Billing/Premium/Models/PremiumUpgradeProration.cs
Normal file
36
src/Core/Billing/Premium/Models/PremiumUpgradeProration.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
namespace Bit.Core.Billing.Premium.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the proration details for upgrading a Premium user subscription to an Organization plan.
|
||||||
|
/// </summary>
|
||||||
|
public class PremiumUpgradeProration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The prorated cost for the new organization plan, calculated from now until the end of the current billing period.
|
||||||
|
/// This represents what the user will pay for the upgraded plan for the remainder of the period.
|
||||||
|
/// </summary>
|
||||||
|
public decimal NewPlanProratedAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The credit amount for the unused portion of the current Premium subscription.
|
||||||
|
/// This credit is applied against the cost of the new organization plan.
|
||||||
|
/// </summary>
|
||||||
|
public decimal Credit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tax amount calculated for the upgrade transaction.
|
||||||
|
/// </summary>
|
||||||
|
public decimal Tax { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The total amount due for the upgrade after applying the credit and adding tax.
|
||||||
|
/// </summary>
|
||||||
|
public decimal Total { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of months the user will be charged for the new organization plan in the prorated billing period.
|
||||||
|
/// Calculated by rounding the days remaining in the current billing cycle to the nearest month.
|
||||||
|
/// Minimum value is 1 month (never returns 0).
|
||||||
|
/// </summary>
|
||||||
|
public int NewPlanProratedMonths { get; set; }
|
||||||
|
}
|
||||||
@ -141,7 +141,6 @@ public static class FeatureFlagKeys
|
|||||||
public const string AutomaticConfirmUsers = "pm-19934-auto-confirm-organization-users";
|
public const string AutomaticConfirmUsers = "pm-19934-auto-confirm-organization-users";
|
||||||
public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache";
|
public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache";
|
||||||
public const string BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration";
|
public const string BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration";
|
||||||
public const string IncreaseBulkReinviteLimitForCloud = "pm-28251-increase-bulk-reinvite-limit-for-cloud";
|
|
||||||
public const string DefaultUserCollectionRestore = "pm-30883-my-items-restored-users";
|
public const string DefaultUserCollectionRestore = "pm-30883-my-items-restored-users";
|
||||||
public const string PremiumAccessQuery = "pm-29495-refactor-premium-interface";
|
public const string PremiumAccessQuery = "pm-29495-refactor-premium-interface";
|
||||||
public const string RefactorMembersComponent = "pm-29503-refactor-members-inheritance";
|
public const string RefactorMembersComponent = "pm-29503-refactor-members-inheritance";
|
||||||
@ -161,7 +160,6 @@ public static class FeatureFlagKeys
|
|||||||
public const string Otp6Digits = "pm-18612-otp-6-digits";
|
public const string Otp6Digits = "pm-18612-otp-6-digits";
|
||||||
public const string PM24579_PreventSsoOnExistingNonCompliantUsers = "pm-24579-prevent-sso-on-existing-non-compliant-users";
|
public const string PM24579_PreventSsoOnExistingNonCompliantUsers = "pm-24579-prevent-sso-on-existing-non-compliant-users";
|
||||||
public const string DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods";
|
public const string DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods";
|
||||||
public const string MJMLBasedEmailTemplates = "mjml-based-email-templates";
|
|
||||||
public const string PM2035PasskeyUnlock = "pm-2035-passkey-unlock";
|
public const string PM2035PasskeyUnlock = "pm-2035-passkey-unlock";
|
||||||
public const string MjmlWelcomeEmailTemplates = "pm-21741-mjml-welcome-email";
|
public const string MjmlWelcomeEmailTemplates = "pm-21741-mjml-welcome-email";
|
||||||
public const string OrganizationConfirmationEmail = "pm-28402-update-confirmed-to-org-email-template";
|
public const string OrganizationConfirmationEmail = "pm-28402-update-confirmed-to-org-email-template";
|
||||||
|
|||||||
@ -1,28 +1,691 @@
|
|||||||
{{#>FullHtmlLayout}}
|
<!doctype html>
|
||||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
<html lang="und" dir="auto" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
<head>
|
||||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
<title></title>
|
||||||
Verify your email to access this Bitwarden Send.
|
<!--[if !mso]><!-->
|
||||||
</td>
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
</tr>
|
<!--<![endif]-->
|
||||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
<style type="text/css">
|
||||||
Your verification code is: <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{Token}}</b>
|
#outlook a { padding:0; }
|
||||||
</td>
|
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
|
||||||
</tr>
|
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
|
||||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
|
||||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
p { display:block;margin:13px 0; }
|
||||||
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
</style>
|
||||||
This code can only be used once and expires in 5 minutes. After that you'll need to verify your email again.
|
<!--[if mso]>
|
||||||
</td>
|
<noscript>
|
||||||
</tr>
|
<xml>
|
||||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
<o:OfficeDocumentSettings>
|
||||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
<o:AllowPNG/>
|
||||||
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
<hr />
|
</o:OfficeDocumentSettings>
|
||||||
{{TheDate}} at {{TheTime}} {{TimeZone}}
|
</xml>
|
||||||
</td>
|
</noscript>
|
||||||
</tr>
|
<![endif]-->
|
||||||
</table>
|
<!--[if lte mso 11]>
|
||||||
{{/FullHtmlLayout}}
|
<style type="text/css">
|
||||||
|
.mj-outlook-group-fix { width:100% !important; }
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (min-width:480px) {
|
||||||
|
.mj-column-per-70 { width:70% !important; max-width: 70%; }
|
||||||
|
.mj-column-per-30 { width:30% !important; max-width: 30%; }
|
||||||
|
.mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||||
|
.mj-column-per-90 { width:90% !important; max-width: 90%; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style media="screen and (min-width:480px)">
|
||||||
|
.moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }
|
||||||
|
.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }
|
||||||
|
.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||||
|
.moz-text-html .mj-column-per-90 { width:90% !important; max-width: 90%; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
|
||||||
|
@media only screen and (max-width:480px) {
|
||||||
|
.mj-bw-hero-responsive-img {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media only screen and (max-width:480px) {
|
||||||
|
.mj-bw-learn-more-footer-responsive-img {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media only screen and (max-width:479px) {
|
||||||
|
table.mj-full-width-mobile { width: 100% !important; }
|
||||||
|
td.mj-full-width-mobile { width: auto !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
.border-fix > table {
|
||||||
|
border-collapse: separate !important;
|
||||||
|
}
|
||||||
|
.border-fix > table > tbody > tr > td {
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.send-bubble {
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
width: 90% !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body style="word-spacing:normal;background-color:#e6e9ef;">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="border-fix" style="background-color:#e6e9ef;" lang="und" dir="auto">
|
||||||
|
<!-- Blue Header Section -->
|
||||||
|
|
||||||
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="border-fix-outlook" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
|
||||||
|
|
||||||
|
<div class="border-fix" style="margin:0px auto;max-width:660px;">
|
||||||
|
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><![endif]-->
|
||||||
|
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
|
||||||
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#175ddc" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
|
||||||
|
|
||||||
|
<div style="margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;">
|
||||||
|
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;border-radius:4px 4px 0px 0px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:434px;" ><![endif]-->
|
||||||
|
|
||||||
|
<div class="mj-column-per-70 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="width:150px;">
|
||||||
|
|
||||||
|
<img alt src="https://bitwarden.com/images/logo-horizontal-white.png" style="border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;" width="150" height="30">
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;">
|
||||||
|
|
||||||
|
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;"><h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
|
||||||
|
Verify your email to access this Bitwarden Send
|
||||||
|
</h1></div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td><td class="" style="vertical-align:bottom;width:186px;" ><![endif]-->
|
||||||
|
|
||||||
|
<div class="mj-column-per-30 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:bottom;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="center" class="mj-bw-hero-responsive-img" style="font-size:0px;padding:0px;word-break:break-word;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="width:155px;">
|
||||||
|
|
||||||
|
<img alt src="https://assets.bitwarden.com/email/v1/spot-secure-send-round.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;" width="155" height="auto">
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
|
||||||
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
|
||||||
|
|
||||||
|
<div style="margin:0px auto;max-width:660px;">
|
||||||
|
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
|
||||||
|
|
||||||
|
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;">
|
||||||
|
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:620px;" ><![endif]-->
|
||||||
|
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="vertical-align:top;padding:0px;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style width="100%">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
|
||||||
|
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;">Your verification code is:</div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
|
||||||
|
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:32px;line-height:1;text-align:left;color:#1B2029;"><b>{{Token}}</b></div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:0px;word-break:break-word;">
|
||||||
|
|
||||||
|
<div style="height:20px;line-height:20px;"> </div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
|
||||||
|
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;">This code expires in {{Expiry}} minutes. After that, you'll need
|
||||||
|
to verify your email again.</div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
|
||||||
|
|
||||||
|
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;">
|
||||||
|
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:0px 0px 20px 0px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="send-bubble-outlook" style="vertical-align:top;width:558px;" ><![endif]-->
|
||||||
|
|
||||||
|
<div class="mj-column-per-90 mj-outlook-group-fix send-bubble" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="vertical-align:top;padding:0px;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color:#DBE5F6;border-radius:16px;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
|
||||||
|
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;"><p>
|
||||||
|
Bitwarden Send transmits sensitive, temporary information to
|
||||||
|
others easily and securely. Learn more about
|
||||||
|
<a href="https://bitwarden.com/help/send" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;">Bitwarden Send</a>
|
||||||
|
or
|
||||||
|
<a href="https://bitwarden.com/signup" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;">sign up</a>
|
||||||
|
to try it today.
|
||||||
|
</p></div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
|
||||||
|
<!-- Learn More Section -->
|
||||||
|
|
||||||
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
|
||||||
|
|
||||||
|
<div style="margin:0px auto;max-width:660px;">
|
||||||
|
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#F3F6F9" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
|
||||||
|
|
||||||
|
<div style="background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;">
|
||||||
|
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:5px 10px 10px 10px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:420px;" ><![endif]-->
|
||||||
|
|
||||||
|
<div class="mj-column-per-70 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
|
||||||
|
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;"><p style="font-size: 18px; line-height: 28px; font-weight: bold;">
|
||||||
|
Learn more about Bitwarden
|
||||||
|
</p>
|
||||||
|
Find user guides, product documentation, and videos on the
|
||||||
|
<a href="https://bitwarden.com/help/" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;"> Bitwarden Help Center</a>.</div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td><td class="" style="vertical-align:top;width:180px;" ><![endif]-->
|
||||||
|
|
||||||
|
<div class="mj-column-per-30 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="center" class="mj-bw-learn-more-footer-responsive-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="width:94px;">
|
||||||
|
|
||||||
|
<img alt src="https://assets.bitwarden.com/email/v1/spot-community.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;" width="94" height="auto">
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
|
||||||
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
|
||||||
|
|
||||||
|
<div style="margin:0px auto;max-width:660px;">
|
||||||
|
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:620px;" ><![endif]-->
|
||||||
|
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="font-size:0px;padding:0;word-break:break-word;">
|
||||||
|
|
||||||
|
|
||||||
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" ><tr><td><![endif]-->
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:8px;vertical-align:middle;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||||
|
<a href="https://x.com/bitwarden" target="_blank">
|
||||||
|
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png" style="border-radius:3px;display:block;" width="24">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!--[if mso | IE]></td><td><![endif]-->
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:8px;vertical-align:middle;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||||
|
<a href="https://www.reddit.com/r/Bitwarden/" target="_blank">
|
||||||
|
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-reddit.png" style="border-radius:3px;display:block;" width="24">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!--[if mso | IE]></td><td><![endif]-->
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:8px;vertical-align:middle;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||||
|
<a href="https://community.bitwarden.com/" target="_blank">
|
||||||
|
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-discourse.png" style="border-radius:3px;display:block;" width="24">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!--[if mso | IE]></td><td><![endif]-->
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:8px;vertical-align:middle;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||||
|
<a href="https://github.com/bitwarden" target="_blank">
|
||||||
|
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-github.png" style="border-radius:3px;display:block;" width="24">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!--[if mso | IE]></td><td><![endif]-->
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:8px;vertical-align:middle;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||||
|
<a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank">
|
||||||
|
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-youtube.png" style="border-radius:3px;display:block;" width="24">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!--[if mso | IE]></td><td><![endif]-->
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:8px;vertical-align:middle;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||||
|
<a href="https://www.linkedin.com/company/bitwarden1/" target="_blank">
|
||||||
|
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-linkedin.png" style="border-radius:3px;display:block;" width="24">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!--[if mso | IE]></td><td><![endif]-->
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:8px;vertical-align:middle;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||||
|
<a href="https://www.facebook.com/bitwarden/" target="_blank">
|
||||||
|
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-facebook.png" style="border-radius:3px;display:block;" width="24">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
|
||||||
|
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;"><p style="margin-bottom: 5px; margin-top: 5px">
|
||||||
|
© 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa
|
||||||
|
Barbara, CA, USA
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 5px">
|
||||||
|
Always confirm you are on a trusted Bitwarden domain before logging
|
||||||
|
in:<br>
|
||||||
|
<a href="https://bitwarden.com/" style="text-decoration:none;color:#175ddc; font-weight:400">bitwarden.com</a> |
|
||||||
|
<a href="https://bitwarden.com/help/emails-from-bitwarden/" style="text-decoration:none; color:#175ddc; font-weight:400">Learn why we include this</a>
|
||||||
|
</p></div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@ -3,7 +3,7 @@ Verify your email to access this Bitwarden Send.
|
|||||||
|
|
||||||
Your verification code is: {{Token}}
|
Your verification code is: {{Token}}
|
||||||
|
|
||||||
This code can only be used once and expires in 5 minutes. After that you'll need to verify your email again.
|
This code can only be used once and expires in {{Expiry}} minutes. After that you'll need to verify your email again.
|
||||||
|
|
||||||
Date : {{TheDate}} at {{TheTime}} {{TimeZone}}
|
Bitwarden Send transmits sensitive, temporary information to others easily and securely. Learn more about Bitwarden Send or sign up to try it today.
|
||||||
{{/BasicTextLayout}}
|
{{/BasicTextLayout}}
|
||||||
|
|||||||
@ -1,691 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="und" dir="auto" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
||||||
<head>
|
|
||||||
<title></title>
|
|
||||||
<!--[if !mso]><!-->
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<!--<![endif]-->
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<style type="text/css">
|
|
||||||
#outlook a { padding:0; }
|
|
||||||
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
|
|
||||||
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
|
|
||||||
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
|
|
||||||
p { display:block;margin:13px 0; }
|
|
||||||
</style>
|
|
||||||
<!--[if mso]>
|
|
||||||
<noscript>
|
|
||||||
<xml>
|
|
||||||
<o:OfficeDocumentSettings>
|
|
||||||
<o:AllowPNG/>
|
|
||||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
||||||
</o:OfficeDocumentSettings>
|
|
||||||
</xml>
|
|
||||||
</noscript>
|
|
||||||
<![endif]-->
|
|
||||||
<!--[if lte mso 11]>
|
|
||||||
<style type="text/css">
|
|
||||||
.mj-outlook-group-fix { width:100% !important; }
|
|
||||||
</style>
|
|
||||||
<![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
<style type="text/css">
|
|
||||||
@media only screen and (min-width:480px) {
|
|
||||||
.mj-column-per-70 { width:70% !important; max-width: 70%; }
|
|
||||||
.mj-column-per-30 { width:30% !important; max-width: 30%; }
|
|
||||||
.mj-column-per-100 { width:100% !important; max-width: 100%; }
|
|
||||||
.mj-column-per-90 { width:90% !important; max-width: 90%; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<style media="screen and (min-width:480px)">
|
|
||||||
.moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }
|
|
||||||
.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }
|
|
||||||
.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }
|
|
||||||
.moz-text-html .mj-column-per-90 { width:90% !important; max-width: 90%; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style type="text/css">
|
|
||||||
|
|
||||||
@media only screen and (max-width:480px) {
|
|
||||||
.mj-bw-hero-responsive-img {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@media only screen and (max-width:480px) {
|
|
||||||
.mj-bw-learn-more-footer-responsive-img {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@media only screen and (max-width:479px) {
|
|
||||||
table.mj-full-width-mobile { width: 100% !important; }
|
|
||||||
td.mj-full-width-mobile { width: auto !important; }
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style type="text/css">
|
|
||||||
.border-fix > table {
|
|
||||||
border-collapse: separate !important;
|
|
||||||
}
|
|
||||||
.border-fix > table > tbody > tr > td {
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
.send-bubble {
|
|
||||||
padding-left: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
width: 90% !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body style="word-spacing:normal;background-color:#e6e9ef;">
|
|
||||||
|
|
||||||
|
|
||||||
<div class="border-fix" style="background-color:#e6e9ef;" lang="und" dir="auto">
|
|
||||||
<!-- Blue Header Section -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="border-fix-outlook" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
<div class="border-fix" style="margin:0px auto;max-width:660px;">
|
|
||||||
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;">
|
|
||||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><![endif]-->
|
|
||||||
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#175ddc" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
<div style="margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;">
|
|
||||||
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;border-radius:4px 4px 0px 0px;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
|
||||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:434px;" ><![endif]-->
|
|
||||||
|
|
||||||
<div class="mj-column-per-70 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="width:150px;">
|
|
||||||
|
|
||||||
<img alt src="https://bitwarden.com/images/logo-horizontal-white.png" style="border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;" width="150" height="30">
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;">
|
|
||||||
|
|
||||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;"><h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
|
|
||||||
Verify your email to access this Bitwarden Send
|
|
||||||
</h1></div>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td><td class="" style="vertical-align:bottom;width:186px;" ><![endif]-->
|
|
||||||
|
|
||||||
<div class="mj-column-per-30 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;">
|
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:bottom;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td align="center" class="mj-bw-hero-responsive-img" style="font-size:0px;padding:0px;word-break:break-word;">
|
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="width:155px;">
|
|
||||||
|
|
||||||
<img alt src="https://assets.bitwarden.com/email/v1/spot-secure-send-round.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;" width="155" height="auto">
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
<div style="margin:0px auto;max-width:660px;">
|
|
||||||
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
|
|
||||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;">
|
|
||||||
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
|
||||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:620px;" ><![endif]-->
|
|
||||||
|
|
||||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="vertical-align:top;padding:0px;">
|
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style width="100%">
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
|
|
||||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;">Your verification code is:</div>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
|
|
||||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:32px;line-height:1;text-align:left;color:#1B2029;"><b>{{Token}}</b></div>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td style="font-size:0px;word-break:break-word;">
|
|
||||||
|
|
||||||
<div style="height:20px;line-height:20px;"> </div>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
|
|
||||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;">This code expires in {{Expiry}} minutes. After that, you'll need
|
|
||||||
to verify your email again.</div>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;">
|
|
||||||
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="direction:ltr;font-size:0px;padding:0px 0px 20px 0px;text-align:center;">
|
|
||||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="send-bubble-outlook" style="vertical-align:top;width:558px;" ><![endif]-->
|
|
||||||
|
|
||||||
<div class="mj-column-per-90 mj-outlook-group-fix send-bubble" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="vertical-align:top;padding:0px;">
|
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color:#DBE5F6;border-radius:16px;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
|
|
||||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;"><p>
|
|
||||||
Bitwarden Send transmits sensitive, temporary information to
|
|
||||||
others easily and securely. Learn more about
|
|
||||||
<a href="https://bitwarden.com/help/send" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;">Bitwarden Send</a>
|
|
||||||
or
|
|
||||||
<a href="https://bitwarden.com/signup" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;">sign up</a>
|
|
||||||
to try it today.
|
|
||||||
</p></div>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
|
|
||||||
<!-- Learn More Section -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
<div style="margin:0px auto;max-width:660px;">
|
|
||||||
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
|
|
||||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#F3F6F9" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
<div style="background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;">
|
|
||||||
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="direction:ltr;font-size:0px;padding:5px 10px 10px 10px;text-align:center;">
|
|
||||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:420px;" ><![endif]-->
|
|
||||||
|
|
||||||
<div class="mj-column-per-70 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
|
|
||||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;"><p style="font-size: 18px; line-height: 28px; font-weight: bold;">
|
|
||||||
Learn more about Bitwarden
|
|
||||||
</p>
|
|
||||||
Find user guides, product documentation, and videos on the
|
|
||||||
<a href="https://bitwarden.com/help/" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;"> Bitwarden Help Center</a>.</div>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td><td class="" style="vertical-align:top;width:180px;" ><![endif]-->
|
|
||||||
|
|
||||||
<div class="mj-column-per-30 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td align="center" class="mj-bw-learn-more-footer-responsive-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="width:94px;">
|
|
||||||
|
|
||||||
<img alt src="https://assets.bitwarden.com/email/v1/spot-community.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;" width="94" height="auto">
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
<div style="margin:0px auto;max-width:660px;">
|
|
||||||
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
|
|
||||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:620px;" ><![endif]-->
|
|
||||||
|
|
||||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="font-size:0px;padding:0;word-break:break-word;">
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" ><tr><td><![endif]-->
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td style="padding:8px;vertical-align:middle;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
|
||||||
<a href="https://x.com/bitwarden" target="_blank">
|
|
||||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png" style="border-radius:3px;display:block;" width="24">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<!--[if mso | IE]></td><td><![endif]-->
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td style="padding:8px;vertical-align:middle;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
|
||||||
<a href="https://www.reddit.com/r/Bitwarden/" target="_blank">
|
|
||||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-reddit.png" style="border-radius:3px;display:block;" width="24">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<!--[if mso | IE]></td><td><![endif]-->
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td style="padding:8px;vertical-align:middle;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
|
||||||
<a href="https://community.bitwarden.com/" target="_blank">
|
|
||||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-discourse.png" style="border-radius:3px;display:block;" width="24">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<!--[if mso | IE]></td><td><![endif]-->
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td style="padding:8px;vertical-align:middle;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
|
||||||
<a href="https://github.com/bitwarden" target="_blank">
|
|
||||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-github.png" style="border-radius:3px;display:block;" width="24">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<!--[if mso | IE]></td><td><![endif]-->
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td style="padding:8px;vertical-align:middle;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
|
||||||
<a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank">
|
|
||||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-youtube.png" style="border-radius:3px;display:block;" width="24">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<!--[if mso | IE]></td><td><![endif]-->
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td style="padding:8px;vertical-align:middle;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
|
||||||
<a href="https://www.linkedin.com/company/bitwarden1/" target="_blank">
|
|
||||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-linkedin.png" style="border-radius:3px;display:block;" width="24">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<!--[if mso | IE]></td><td><![endif]-->
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td style="padding:8px;vertical-align:middle;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
|
||||||
<a href="https://www.facebook.com/bitwarden/" target="_blank">
|
|
||||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-facebook.png" style="border-radius:3px;display:block;" width="24">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
|
|
||||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;"><p style="margin-bottom: 5px; margin-top: 5px">
|
|
||||||
© 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa
|
|
||||||
Barbara, CA, USA
|
|
||||||
</p>
|
|
||||||
<p style="margin-top: 5px">
|
|
||||||
Always confirm you are on a trusted Bitwarden domain before logging
|
|
||||||
in:<br>
|
|
||||||
<a href="https://bitwarden.com/" style="text-decoration:none;color:#175ddc; font-weight:400">bitwarden.com</a> |
|
|
||||||
<a href="https://bitwarden.com/help/emails-from-bitwarden/" style="text-decoration:none; color:#175ddc; font-weight:400">Learn why we include this</a>
|
|
||||||
</p></div>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
{{#>BasicTextLayout}}
|
|
||||||
Verify your email to access this Bitwarden Send.
|
|
||||||
|
|
||||||
Your verification code is: {{Token}}
|
|
||||||
|
|
||||||
This code can only be used once and expires in {{Expiry}} minutes. After that you'll need to verify your email again.
|
|
||||||
|
|
||||||
Bitwarden Send transmits sensitive, temporary information to others easily and securely. Learn more about Bitwarden Send or sign up to try it today.
|
|
||||||
{{/BasicTextLayout}}
|
|
||||||
@ -5,6 +5,7 @@ using Bit.Core.AdminConsole.Entities;
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;
|
using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
@ -30,6 +31,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
|||||||
private readonly IGroupRepository _groupRepository;
|
private readonly IGroupRepository _groupRepository;
|
||||||
private readonly IStripePaymentService _paymentService;
|
private readonly IStripePaymentService _paymentService;
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyRepository _policyRepository;
|
||||||
|
private readonly IPolicyQuery _policyQuery;
|
||||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||||
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
|
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
|
||||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||||
@ -45,6 +47,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
|||||||
IGroupRepository groupRepository,
|
IGroupRepository groupRepository,
|
||||||
IStripePaymentService paymentService,
|
IStripePaymentService paymentService,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
|
IPolicyQuery policyQuery,
|
||||||
ISsoConfigRepository ssoConfigRepository,
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
IOrganizationConnectionRepository organizationConnectionRepository,
|
IOrganizationConnectionRepository organizationConnectionRepository,
|
||||||
IServiceAccountRepository serviceAccountRepository,
|
IServiceAccountRepository serviceAccountRepository,
|
||||||
@ -59,6 +62,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
|||||||
_groupRepository = groupRepository;
|
_groupRepository = groupRepository;
|
||||||
_paymentService = paymentService;
|
_paymentService = paymentService;
|
||||||
_policyRepository = policyRepository;
|
_policyRepository = policyRepository;
|
||||||
|
_policyQuery = policyQuery;
|
||||||
_ssoConfigRepository = ssoConfigRepository;
|
_ssoConfigRepository = ssoConfigRepository;
|
||||||
_organizationConnectionRepository = organizationConnectionRepository;
|
_organizationConnectionRepository = organizationConnectionRepository;
|
||||||
_serviceAccountRepository = serviceAccountRepository;
|
_serviceAccountRepository = serviceAccountRepository;
|
||||||
@ -184,9 +188,8 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
|||||||
|
|
||||||
if (!newPlan.HasResetPassword && organization.UseResetPassword)
|
if (!newPlan.HasResetPassword && organization.UseResetPassword)
|
||||||
{
|
{
|
||||||
var resetPasswordPolicy =
|
var resetPasswordPolicy = await _policyQuery.RunAsync(organization.Id, PolicyType.ResetPassword);
|
||||||
await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
|
if (resetPasswordPolicy.Enabled)
|
||||||
if (resetPasswordPolicy != null && resetPasswordPolicy.Enabled)
|
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Your new plan does not allow the Password Reset feature. " +
|
throw new BadRequestException("Your new plan does not allow the Password Reset feature. " +
|
||||||
"Disable your Password Reset policy.");
|
"Disable your Password Reset policy.");
|
||||||
|
|||||||
@ -209,26 +209,6 @@ public class HandlebarsMailService : IMailService
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendSendEmailOtpEmailAsync(string email, string token, string subject)
|
public async Task SendSendEmailOtpEmailAsync(string email, string token, string subject)
|
||||||
{
|
|
||||||
var message = CreateDefaultMessage(subject, email);
|
|
||||||
var requestDateTime = DateTime.UtcNow;
|
|
||||||
var model = new DefaultEmailOtpViewModel
|
|
||||||
{
|
|
||||||
Token = token,
|
|
||||||
TheDate = requestDateTime.ToLongDateString(),
|
|
||||||
TheTime = requestDateTime.ToShortTimeString(),
|
|
||||||
TimeZone = _utcTimeZoneDisplay,
|
|
||||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
|
||||||
SiteName = _globalSettings.SiteName,
|
|
||||||
};
|
|
||||||
await AddMessageContentAsync(message, "Auth.SendAccessEmailOtpEmail", model);
|
|
||||||
message.MetaData.Add("SendGridBypassListManagement", true);
|
|
||||||
// TODO - PM-25380 change to string constant
|
|
||||||
message.Category = "SendEmailOtp";
|
|
||||||
await _mailDeliveryService.SendEmailAsync(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendSendEmailOtpEmailv2Async(string email, string token, string subject)
|
|
||||||
{
|
{
|
||||||
var message = CreateDefaultMessage(subject, email);
|
var message = CreateDefaultMessage(subject, email);
|
||||||
var requestDateTime = DateTime.UtcNow;
|
var requestDateTime = DateTime.UtcNow;
|
||||||
@ -242,7 +222,7 @@ public class HandlebarsMailService : IMailService
|
|||||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||||
SiteName = _globalSettings.SiteName,
|
SiteName = _globalSettings.SiteName,
|
||||||
};
|
};
|
||||||
await AddMessageContentAsync(message, "Auth.SendAccessEmailOtpEmailv2", model);
|
await AddMessageContentAsync(message, "Auth.SendAccessEmailOtpEmail", model);
|
||||||
message.MetaData.Add("SendGridBypassListManagement", true);
|
message.MetaData.Add("SendGridBypassListManagement", true);
|
||||||
// TODO - PM-25380 change to string constant
|
// TODO - PM-25380 change to string constant
|
||||||
message.Category = "SendEmailOtp";
|
message.Category = "SendEmailOtp";
|
||||||
|
|||||||
@ -51,17 +51,15 @@ public interface IMailService
|
|||||||
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
|
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
|
||||||
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
|
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
|
||||||
Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose);
|
Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose);
|
||||||
Task SendSendEmailOtpEmailAsync(string email, string token, string subject);
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <see cref="DefaultOtpTokenProviderOptions"/> has a default expiry of 5 minutes so we set the expiry to that value int he view model.
|
/// <see cref="DefaultOtpTokenProviderOptions"/> has a default expiry of 5 minutes so we set the expiry to that value in the view model.
|
||||||
/// Sends OTP code token to the specified email address.
|
/// Sends OTP code token to the specified email address.
|
||||||
/// will replace <see cref="SendSendEmailOtpEmailAsync"/> when MJML templates are fully accepted.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="email">Email address to send the OTP to</param>
|
/// <param name="email">Email address to send the OTP to</param>
|
||||||
/// <param name="token">Otp code token</param>
|
/// <param name="token">Otp code token</param>
|
||||||
/// <param name="subject">subject line of the email</param>
|
/// <param name="subject">Subject line of the email</param>
|
||||||
/// <returns>Task</returns>
|
/// <returns>Task</returns>
|
||||||
Task SendSendEmailOtpEmailv2Async(string email, string token, string subject);
|
Task SendSendEmailOtpEmailAsync(string email, string token, string subject);
|
||||||
Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType type, DateTime utcNow, string ip);
|
Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType type, DateTime utcNow, string ip);
|
||||||
Task SendNoMasterPasswordHintEmailAsync(string email);
|
Task SendNoMasterPasswordHintEmailAsync(string email);
|
||||||
Task SendMasterPasswordHintEmailAsync(string email, string hint);
|
Task SendMasterPasswordHintEmailAsync(string email, string hint);
|
||||||
|
|||||||
@ -99,11 +99,6 @@ public class NoopMailService : IMailService
|
|||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SendSendEmailOtpEmailv2Async(string email, string token, string subject)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)
|
public Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
|
|||||||
@ -61,7 +61,7 @@ public class UserService : UserManager<User>, IUserService
|
|||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private readonly IStripePaymentService _paymentService;
|
private readonly IStripePaymentService _paymentService;
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyQuery _policyQuery;
|
||||||
private readonly IPolicyService _policyService;
|
private readonly IPolicyService _policyService;
|
||||||
private readonly IFido2 _fido2;
|
private readonly IFido2 _fido2;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
@ -98,7 +98,7 @@ public class UserService : UserManager<User>, IUserService
|
|||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
IStripePaymentService paymentService,
|
IStripePaymentService paymentService,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyQuery policyQuery,
|
||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
IFido2 fido2,
|
IFido2 fido2,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@ -139,7 +139,7 @@ public class UserService : UserManager<User>, IUserService
|
|||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_paymentService = paymentService;
|
_paymentService = paymentService;
|
||||||
_policyRepository = policyRepository;
|
_policyQuery = policyQuery;
|
||||||
_policyService = policyService;
|
_policyService = policyService;
|
||||||
_fido2 = fido2;
|
_fido2 = fido2;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
@ -722,9 +722,8 @@ public class UserService : UserManager<User>, IUserService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enterprise policy must be enabled
|
// Enterprise policy must be enabled
|
||||||
var resetPasswordPolicy =
|
var resetPasswordPolicy = await _policyQuery.RunAsync(orgId, PolicyType.ResetPassword);
|
||||||
await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
if (!resetPasswordPolicy.Enabled)
|
||||||
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled)
|
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Organization does not have the password reset policy enabled.");
|
throw new BadRequestException("Organization does not have the password reset policy enabled.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,6 +76,12 @@ public class ImportCiphersCommand : IImportCiphersCommand
|
|||||||
{
|
{
|
||||||
cipher.Favorites = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":true}}";
|
cipher.Favorites = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":true}}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cipher.UserId.HasValue && cipher.ArchivedDate.HasValue)
|
||||||
|
{
|
||||||
|
cipher.Archives = $"{{\"{cipher.UserId.Value.ToString().ToUpperInvariant()}\":\"" +
|
||||||
|
$"{cipher.ArchivedDate.Value:yyyy-MM-ddTHH:mm:ss.fffffffZ}\"}}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var userfoldersIds = (await _folderRepository.GetManyByUserIdAsync(importingUserId)).Select(f => f.Id).ToList();
|
var userfoldersIds = (await _folderRepository.GetManyByUserIdAsync(importingUserId)).Select(f => f.Id).ToList();
|
||||||
@ -135,10 +141,16 @@ public class ImportCiphersCommand : IImportCiphersCommand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init. ids for ciphers
|
|
||||||
foreach (var cipher in ciphers)
|
foreach (var cipher in ciphers)
|
||||||
{
|
{
|
||||||
|
// Init. ids for ciphers
|
||||||
cipher.SetNewId();
|
cipher.SetNewId();
|
||||||
|
|
||||||
|
if (cipher.ArchivedDate.HasValue)
|
||||||
|
{
|
||||||
|
cipher.Archives = $"{{\"{importingUserId.ToString().ToUpperInvariant()}\":\"" +
|
||||||
|
$"{cipher.ArchivedDate.Value:yyyy-MM-ddTHH:mm:ss.fffffffZ}\"}}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var organizationCollectionsIds = (await _collectionRepository.GetManyByOrganizationIdAsync(org.Id)).Select(c => c.Id).ToList();
|
var organizationCollectionsIds = (await _collectionRepository.GetManyByOrganizationIdAsync(org.Id)).Select(c => c.Id).ToList();
|
||||||
|
|||||||
@ -44,7 +44,7 @@ public record ResourcePassword(string Hash) : SendAuthenticationMethod;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a send claim by requesting a one time password (OTP) confirmation code.
|
/// Create a send claim by requesting a one time password (OTP) confirmation code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Emails">
|
/// <param name="EmailHashes">
|
||||||
/// The list of email address **hashes** permitted access to the send.
|
/// The list of email address **hashes** permitted access to the send.
|
||||||
/// </param>
|
/// </param>
|
||||||
public record EmailOtp(string[] Emails) : SendAuthenticationMethod;
|
public record EmailOtp(string[] EmailHashes) : SendAuthenticationMethod;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
<Sdk Name="Bitwarden.Server.Sdk" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<UserSecretsId>bitwarden-Events</UserSecretsId>
|
<UserSecretsId>bitwarden-Events</UserSecretsId>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ public class Program
|
|||||||
{
|
{
|
||||||
Host
|
Host
|
||||||
.CreateDefaultBuilder(args)
|
.CreateDefaultBuilder(args)
|
||||||
.ConfigureCustomAppConfiguration(args)
|
.UseBitwardenSdk()
|
||||||
.ConfigureWebHostDefaults(webBuilder =>
|
.ConfigureWebHostDefaults(webBuilder =>
|
||||||
{
|
{
|
||||||
webBuilder.UseStartup<Startup>();
|
webBuilder.UseStartup<Startup>();
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
<Sdk Name="Bitwarden.Server.Sdk" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<UserSecretsId>bitwarden-EventsProcessor</UserSecretsId>
|
<UserSecretsId>bitwarden-EventsProcessor</UserSecretsId>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.SharedWeb.Utilities;
|
using Bit.SharedWeb.Utilities;
|
||||||
using Microsoft.IdentityModel.Logging;
|
|
||||||
|
|
||||||
namespace Bit.EventsProcessor;
|
namespace Bit.EventsProcessor;
|
||||||
|
|
||||||
@ -40,7 +39,6 @@ public class Startup
|
|||||||
|
|
||||||
public void Configure(IApplicationBuilder app)
|
public void Configure(IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
IdentityModelEventSource.ShowPII = true;
|
|
||||||
// Add general security headers
|
// Add general security headers
|
||||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
<Sdk Name="Bitwarden.Server.Sdk" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<UserSecretsId>bitwarden-Icons</UserSecretsId>
|
<UserSecretsId>bitwarden-Icons</UserSecretsId>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ public class Program
|
|||||||
{
|
{
|
||||||
Host
|
Host
|
||||||
.CreateDefaultBuilder(args)
|
.CreateDefaultBuilder(args)
|
||||||
|
.UseBitwardenSdk()
|
||||||
.ConfigureWebHostDefaults(webBuilder =>
|
.ConfigureWebHostDefaults(webBuilder =>
|
||||||
{
|
{
|
||||||
webBuilder.UseStartup<Startup>();
|
webBuilder.UseStartup<Startup>();
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
<Sdk Name="Bitwarden.Server.Sdk" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<UserSecretsId>bitwarden-Identity</UserSecretsId>
|
<UserSecretsId>bitwarden-Identity</UserSecretsId>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Bit.Core;
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
using Bit.Core.Auth.Identity;
|
using Bit.Core.Auth.Identity;
|
||||||
using Bit.Core.Auth.Identity.TokenProviders;
|
using Bit.Core.Auth.Identity.TokenProviders;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -11,7 +12,6 @@ using Duende.IdentityServer.Validation;
|
|||||||
namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||||
|
|
||||||
public class SendEmailOtpRequestValidator(
|
public class SendEmailOtpRequestValidator(
|
||||||
IFeatureService featureService,
|
|
||||||
IOtpTokenProvider<DefaultOtpTokenProviderOptions> otpTokenProvider,
|
IOtpTokenProvider<DefaultOtpTokenProviderOptions> otpTokenProvider,
|
||||||
IMailService mailService) : ISendAuthenticationMethodValidator<EmailOtp>
|
IMailService mailService) : ISendAuthenticationMethodValidator<EmailOtp>
|
||||||
{
|
{
|
||||||
@ -40,8 +40,10 @@ public class SendEmailOtpRequestValidator(
|
|||||||
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailRequired);
|
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
// email must be in the list of emails in the EmailOtp array
|
// email hash must be in the list of email hashes in the EmailOtp array
|
||||||
if (!authMethod.Emails.Contains(email))
|
byte[] hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(email));
|
||||||
|
string hashEmailHex = Convert.ToHexString(hashBytes).ToUpperInvariant();
|
||||||
|
if (!authMethod.EmailHashes.Contains(hashEmailHex))
|
||||||
{
|
{
|
||||||
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid);
|
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid);
|
||||||
}
|
}
|
||||||
@ -62,20 +64,12 @@ public class SendEmailOtpRequestValidator(
|
|||||||
{
|
{
|
||||||
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.OtpGenerationFailed);
|
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.OtpGenerationFailed);
|
||||||
}
|
}
|
||||||
if (featureService.IsEnabled(FeatureFlagKeys.MJMLBasedEmailTemplates))
|
|
||||||
{
|
await mailService.SendSendEmailOtpEmailAsync(
|
||||||
await mailService.SendSendEmailOtpEmailv2Async(
|
email,
|
||||||
email,
|
token,
|
||||||
token,
|
string.Format(SendAccessConstants.OtpEmail.Subject, token));
|
||||||
string.Format(SendAccessConstants.OtpEmail.Subject, token));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await mailService.SendSendEmailOtpEmailAsync(
|
|
||||||
email,
|
|
||||||
token,
|
|
||||||
string.Format(SendAccessConstants.OtpEmail.Subject, token));
|
|
||||||
}
|
|
||||||
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailOtpSent);
|
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailOtpSent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ public class Program
|
|||||||
{
|
{
|
||||||
return Host
|
return Host
|
||||||
.CreateDefaultBuilder(args)
|
.CreateDefaultBuilder(args)
|
||||||
.ConfigureCustomAppConfiguration(args)
|
.UseBitwardenSdk()
|
||||||
.ConfigureWebHostDefaults(webBuilder =>
|
.ConfigureWebHostDefaults(webBuilder =>
|
||||||
{
|
{
|
||||||
webBuilder.UseStartup<Startup>();
|
webBuilder.UseStartup<Startup>();
|
||||||
|
|||||||
@ -14,7 +14,6 @@ using Bit.SharedWeb.Swagger;
|
|||||||
using Bit.SharedWeb.Utilities;
|
using Bit.SharedWeb.Utilities;
|
||||||
using Duende.IdentityServer.Services;
|
using Duende.IdentityServer.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.IdentityModel.Logging;
|
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
namespace Bit.Identity;
|
namespace Bit.Identity;
|
||||||
@ -170,16 +169,14 @@ public class Startup
|
|||||||
|
|
||||||
public void Configure(
|
public void Configure(
|
||||||
IApplicationBuilder app,
|
IApplicationBuilder app,
|
||||||
IWebHostEnvironment env,
|
IWebHostEnvironment environment,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
ILogger<Startup> logger)
|
ILogger<Startup> logger)
|
||||||
{
|
{
|
||||||
IdentityModelEventSource.ShowPII = true;
|
|
||||||
|
|
||||||
// Add general security headers
|
// Add general security headers
|
||||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||||
|
|
||||||
if (!env.IsDevelopment())
|
if (!environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
var uri = new Uri(globalSettings.BaseServiceUri.Identity);
|
var uri = new Uri(globalSettings.BaseServiceUri.Identity);
|
||||||
app.Use(async (ctx, next) =>
|
app.Use(async (ctx, next) =>
|
||||||
@ -196,7 +193,7 @@ public class Startup
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default Middleware
|
// Default Middleware
|
||||||
app.UseDefaultMiddleware(env, globalSettings);
|
app.UseDefaultMiddleware(environment, globalSettings);
|
||||||
|
|
||||||
if (!globalSettings.SelfHosted)
|
if (!globalSettings.SelfHosted)
|
||||||
{
|
{
|
||||||
@ -204,7 +201,7 @@ public class Startup
|
|||||||
app.UseMiddleware<CustomIpRateLimitMiddleware>();
|
app.UseMiddleware<CustomIpRateLimitMiddleware>();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.IsDevelopment())
|
if (environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
|
|||||||
@ -248,7 +248,7 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
|||||||
new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId },
|
new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId },
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
return results;
|
return DateTime.SpecifyKind(results, DateTimeKind.Utc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -595,7 +595,7 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
|||||||
new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId },
|
new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId },
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
return results;
|
return DateTime.SpecifyKind(results, DateTimeKind.Utc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -608,7 +608,7 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
|||||||
new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId },
|
new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId },
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
return results;
|
return DateTime.SpecifyKind(results, DateTimeKind.Utc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -621,7 +621,7 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
|||||||
new { Ids = ids.ToGuidIdArrayTVP(), OrganizationId = organizationId },
|
new { Ids = ids.ToGuidIdArrayTVP(), OrganizationId = organizationId },
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
return results;
|
return DateTime.SpecifyKind(results, DateTimeKind.Utc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
<Sdk Name="Bitwarden.Server.Sdk" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<UserSecretsId>bitwarden-Notifications</UserSecretsId>
|
<UserSecretsId>bitwarden-Notifications</UserSecretsId>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ public class Program
|
|||||||
{
|
{
|
||||||
Host
|
Host
|
||||||
.CreateDefaultBuilder(args)
|
.CreateDefaultBuilder(args)
|
||||||
.ConfigureCustomAppConfiguration(args)
|
.UseBitwardenSdk()
|
||||||
.ConfigureWebHostDefaults(webBuilder =>
|
.ConfigureWebHostDefaults(webBuilder =>
|
||||||
{
|
{
|
||||||
webBuilder.UseStartup<Startup>();
|
webBuilder.UseStartup<Startup>();
|
||||||
|
|||||||
@ -5,7 +5,6 @@ using Bit.Core.Utilities;
|
|||||||
using Bit.SharedWeb.Utilities;
|
using Bit.SharedWeb.Utilities;
|
||||||
using Duende.IdentityModel;
|
using Duende.IdentityModel;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.IdentityModel.Logging;
|
|
||||||
|
|
||||||
namespace Bit.Notifications;
|
namespace Bit.Notifications;
|
||||||
|
|
||||||
@ -84,8 +83,6 @@ public class Startup
|
|||||||
IWebHostEnvironment env,
|
IWebHostEnvironment env,
|
||||||
GlobalSettings globalSettings)
|
GlobalSettings globalSettings)
|
||||||
{
|
{
|
||||||
IdentityModelEventSource.ShowPII = true;
|
|
||||||
|
|
||||||
// Add general security headers
|
// Add general security headers
|
||||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||||
|
|
||||||
|
|||||||
@ -472,11 +472,6 @@ public static class ServiceCollectionExtensions
|
|||||||
addAuthorization.Invoke(config);
|
addAuthorization.Invoke(config);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddCustomDataProtectionServices(
|
public static void AddCustomDataProtectionServices(
|
||||||
@ -666,7 +661,6 @@ public static class ServiceCollectionExtensions
|
|||||||
Constants.BrowserExtensions.OperaId
|
Constants.BrowserExtensions.OperaId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using AutoMapper;
|
||||||
using Bit.Api.AdminConsole.Models.Request;
|
using Bit.Api.AdminConsole.Models.Request;
|
||||||
using Bit.Api.IntegrationTest.Factories;
|
using Bit.Api.IntegrationTest.Factories;
|
||||||
using Bit.Api.IntegrationTest.Helpers;
|
using Bit.Api.IntegrationTest.Helpers;
|
||||||
using Bit.Api.Models.Request;
|
using Bit.Api.Models.Request;
|
||||||
|
using Bit.Core.Entities;
|
||||||
using Bit.Seeder.Recipes;
|
using Bit.Seeder.Recipes;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
@ -26,7 +29,9 @@ public class GroupsControllerPerformanceTests(ITestOutputHelper testOutputHelper
|
|||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
var mapper = factory.GetService<IMapper>();
|
||||||
|
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
|
||||||
|
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
|
||||||
var collectionsSeeder = new CollectionsRecipe(db);
|
var collectionsSeeder = new CollectionsRecipe(db);
|
||||||
var groupsSeeder = new GroupsRecipe(db);
|
var groupsSeeder = new GroupsRecipe(db);
|
||||||
|
|
||||||
@ -34,8 +39,8 @@ public class GroupsControllerPerformanceTests(ITestOutputHelper testOutputHelper
|
|||||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
|
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
|
||||||
|
|
||||||
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
||||||
var collectionIds = collectionsSeeder.AddToOrganization(orgId, collectionCount, orgUserIds, 0);
|
var collectionIds = collectionsSeeder.Seed(orgId, collectionCount, orgUserIds, 0);
|
||||||
var groupIds = groupsSeeder.AddToOrganization(orgId, 1, orgUserIds, 0);
|
var groupIds = groupsSeeder.Seed(orgId, 1, orgUserIds, 0);
|
||||||
|
|
||||||
var groupId = groupIds.First();
|
var groupId = groupIds.First();
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using AutoMapper;
|
||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Api.IntegrationTest.Factories;
|
using Bit.Api.IntegrationTest.Factories;
|
||||||
using Bit.Api.IntegrationTest.Helpers;
|
using Bit.Api.IntegrationTest.Helpers;
|
||||||
using Bit.Api.Models.Request;
|
using Bit.Api.Models.Request;
|
||||||
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Seeder.Recipes;
|
using Bit.Seeder.Recipes;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
@ -28,7 +31,9 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
var mapper = factory.GetService<IMapper>();
|
||||||
|
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
|
||||||
|
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
|
||||||
var collectionsSeeder = new CollectionsRecipe(db);
|
var collectionsSeeder = new CollectionsRecipe(db);
|
||||||
var groupsSeeder = new GroupsRecipe(db);
|
var groupsSeeder = new GroupsRecipe(db);
|
||||||
|
|
||||||
@ -37,8 +42,8 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: seats);
|
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: seats);
|
||||||
|
|
||||||
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
||||||
collectionsSeeder.AddToOrganization(orgId, 10, orgUserIds);
|
collectionsSeeder.Seed(orgId, 10, orgUserIds);
|
||||||
groupsSeeder.AddToOrganization(orgId, 5, orgUserIds);
|
groupsSeeder.Seed(orgId, 5, orgUserIds);
|
||||||
|
|
||||||
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
||||||
|
|
||||||
@ -64,7 +69,9 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
var mapper = factory.GetService<IMapper>();
|
||||||
|
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
|
||||||
|
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
|
||||||
var collectionsSeeder = new CollectionsRecipe(db);
|
var collectionsSeeder = new CollectionsRecipe(db);
|
||||||
var groupsSeeder = new GroupsRecipe(db);
|
var groupsSeeder = new GroupsRecipe(db);
|
||||||
|
|
||||||
@ -72,8 +79,8 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: seats);
|
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: seats);
|
||||||
|
|
||||||
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
||||||
collectionsSeeder.AddToOrganization(orgId, 10, orgUserIds);
|
collectionsSeeder.Seed(orgId, 10, orgUserIds);
|
||||||
groupsSeeder.AddToOrganization(orgId, 5, orgUserIds);
|
groupsSeeder.Seed(orgId, 5, orgUserIds);
|
||||||
|
|
||||||
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
||||||
|
|
||||||
@ -98,14 +105,16 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
var mapper = factory.GetService<IMapper>();
|
||||||
|
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
|
||||||
|
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
|
||||||
var groupsSeeder = new GroupsRecipe(db);
|
var groupsSeeder = new GroupsRecipe(db);
|
||||||
|
|
||||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: 1);
|
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: 1);
|
||||||
|
|
||||||
var orgUserId = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).FirstOrDefault();
|
var orgUserId = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).FirstOrDefault();
|
||||||
groupsSeeder.AddToOrganization(orgId, 2, [orgUserId]);
|
groupsSeeder.Seed(orgId, 2, [orgUserId]);
|
||||||
|
|
||||||
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
||||||
|
|
||||||
@ -130,7 +139,9 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
var mapper = factory.GetService<IMapper>();
|
||||||
|
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
|
||||||
|
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
|
||||||
|
|
||||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: 1);
|
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: 1);
|
||||||
@ -163,7 +174,9 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
var mapper = factory.GetService<IMapper>();
|
||||||
|
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
|
||||||
|
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
|
||||||
|
|
||||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||||
var orgId = orgSeeder.Seed(
|
var orgId = orgSeeder.Seed(
|
||||||
@ -211,7 +224,9 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
var mapper = factory.GetService<IMapper>();
|
||||||
|
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
|
||||||
|
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
|
||||||
|
|
||||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
|
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
|
||||||
@ -251,7 +266,9 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
var mapper = factory.GetService<IMapper>();
|
||||||
|
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
|
||||||
|
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
|
||||||
|
|
||||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||||
var orgId = orgSeeder.Seed(
|
var orgId = orgSeeder.Seed(
|
||||||
@ -295,7 +312,9 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
var mapper = factory.GetService<IMapper>();
|
||||||
|
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
|
||||||
|
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
|
||||||
|
|
||||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||||
var orgId = orgSeeder.Seed(
|
var orgId = orgSeeder.Seed(
|
||||||
@ -339,7 +358,9 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
var mapper = factory.GetService<IMapper>();
|
||||||
|
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
|
||||||
|
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
|
||||||
var domainSeeder = new OrganizationDomainRecipe(db);
|
var domainSeeder = new OrganizationDomainRecipe(db);
|
||||||
|
|
||||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||||
@ -350,7 +371,7 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
users: userCount,
|
users: userCount,
|
||||||
usersStatus: OrganizationUserStatusType.Confirmed);
|
usersStatus: OrganizationUserStatusType.Confirmed);
|
||||||
|
|
||||||
domainSeeder.AddVerifiedDomainToOrganization(orgId, domain);
|
domainSeeder.Seed(orgId, domain);
|
||||||
|
|
||||||
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
||||||
|
|
||||||
@ -384,7 +405,9 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
var mapper = factory.GetService<IMapper>();
|
||||||
|
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
|
||||||
|
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
|
||||||
var collectionsSeeder = new CollectionsRecipe(db);
|
var collectionsSeeder = new CollectionsRecipe(db);
|
||||||
var groupsSeeder = new GroupsRecipe(db);
|
var groupsSeeder = new GroupsRecipe(db);
|
||||||
|
|
||||||
@ -392,8 +415,8 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: 1);
|
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: 1);
|
||||||
|
|
||||||
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
||||||
var collectionIds = collectionsSeeder.AddToOrganization(orgId, 3, orgUserIds, 0);
|
var collectionIds = collectionsSeeder.Seed(orgId, 3, orgUserIds, 0);
|
||||||
var groupIds = groupsSeeder.AddToOrganization(orgId, 2, orgUserIds, 0);
|
var groupIds = groupsSeeder.Seed(orgId, 2, orgUserIds, 0);
|
||||||
|
|
||||||
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
||||||
|
|
||||||
@ -434,7 +457,9 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
var mapper = factory.GetService<IMapper>();
|
||||||
|
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
|
||||||
|
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
|
||||||
|
|
||||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
|
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
|
||||||
@ -471,7 +496,9 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
var mapper = factory.GetService<IMapper>();
|
||||||
|
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
|
||||||
|
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
|
||||||
var domainSeeder = new OrganizationDomainRecipe(db);
|
var domainSeeder = new OrganizationDomainRecipe(db);
|
||||||
|
|
||||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||||
@ -481,7 +508,7 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
users: 2,
|
users: 2,
|
||||||
usersStatus: OrganizationUserStatusType.Confirmed);
|
usersStatus: OrganizationUserStatusType.Confirmed);
|
||||||
|
|
||||||
domainSeeder.AddVerifiedDomainToOrganization(orgId, domain);
|
domainSeeder.Seed(orgId, domain);
|
||||||
|
|
||||||
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
||||||
|
|
||||||
@ -512,14 +539,16 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
var mapper = factory.GetService<IMapper>();
|
||||||
|
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
|
||||||
|
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
|
||||||
var collectionsSeeder = new CollectionsRecipe(db);
|
var collectionsSeeder = new CollectionsRecipe(db);
|
||||||
|
|
||||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: 1);
|
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: 1);
|
||||||
|
|
||||||
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
||||||
var collectionIds = collectionsSeeder.AddToOrganization(orgId, 2, orgUserIds, 0);
|
var collectionIds = collectionsSeeder.Seed(orgId, 2, orgUserIds, 0);
|
||||||
|
|
||||||
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
||||||
|
|
||||||
@ -560,7 +589,9 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
|||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
var mapper = factory.GetService<IMapper>();
|
||||||
|
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
|
||||||
|
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
|
||||||
|
|
||||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||||
var orgId = orgSeeder.Seed(
|
var orgId = orgSeeder.Seed(
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using AutoMapper;
|
||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Api.Auth.Models.Request.Accounts;
|
using Bit.Api.Auth.Models.Request.Accounts;
|
||||||
using Bit.Api.IntegrationTest.Factories;
|
using Bit.Api.IntegrationTest.Factories;
|
||||||
using Bit.Api.IntegrationTest.Helpers;
|
using Bit.Api.IntegrationTest.Helpers;
|
||||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
using Bit.Seeder.Recipes;
|
using Bit.Seeder.Recipes;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
@ -29,7 +32,9 @@ public class OrganizationsControllerPerformanceTests(ITestOutputHelper testOutpu
|
|||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
var mapper = factory.GetService<IMapper>();
|
||||||
|
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
|
||||||
|
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
|
||||||
var collectionsSeeder = new CollectionsRecipe(db);
|
var collectionsSeeder = new CollectionsRecipe(db);
|
||||||
var groupsSeeder = new GroupsRecipe(db);
|
var groupsSeeder = new GroupsRecipe(db);
|
||||||
|
|
||||||
@ -37,8 +42,8 @@ public class OrganizationsControllerPerformanceTests(ITestOutputHelper testOutpu
|
|||||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
|
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
|
||||||
|
|
||||||
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
||||||
collectionsSeeder.AddToOrganization(orgId, collectionCount, orgUserIds, 0);
|
collectionsSeeder.Seed(orgId, collectionCount, orgUserIds, 0);
|
||||||
groupsSeeder.AddToOrganization(orgId, groupCount, orgUserIds, 0);
|
groupsSeeder.Seed(orgId, groupCount, orgUserIds, 0);
|
||||||
|
|
||||||
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
||||||
|
|
||||||
@ -77,7 +82,9 @@ public class OrganizationsControllerPerformanceTests(ITestOutputHelper testOutpu
|
|||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
var mapper = factory.GetService<IMapper>();
|
||||||
|
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
|
||||||
|
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
|
||||||
var collectionsSeeder = new CollectionsRecipe(db);
|
var collectionsSeeder = new CollectionsRecipe(db);
|
||||||
var groupsSeeder = new GroupsRecipe(db);
|
var groupsSeeder = new GroupsRecipe(db);
|
||||||
|
|
||||||
@ -85,8 +92,8 @@ public class OrganizationsControllerPerformanceTests(ITestOutputHelper testOutpu
|
|||||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
|
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
|
||||||
|
|
||||||
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
||||||
collectionsSeeder.AddToOrganization(orgId, collectionCount, orgUserIds, 0);
|
collectionsSeeder.Seed(orgId, collectionCount, orgUserIds, 0);
|
||||||
groupsSeeder.AddToOrganization(orgId, groupCount, orgUserIds, 0);
|
groupsSeeder.Seed(orgId, groupCount, orgUserIds, 0);
|
||||||
|
|
||||||
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
|
||||||
using Bit.Core.AdminConsole.Utilities.v2.Results;
|
using Bit.Core.AdminConsole.Utilities.v2.Results;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
@ -30,6 +29,7 @@ using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
|||||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||||
using Bit.Core.Utilities;
|
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;
|
||||||
@ -137,23 +137,20 @@ public class OrganizationUsersControllerTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task Accept_WhenOrganizationUsePoliciesIsEnabledAndResetPolicyIsEnabled_ShouldHandleResetPassword(Guid orgId, Guid orgUserId,
|
public async Task Accept_WhenOrganizationUsePoliciesIsEnabledAndResetPolicyIsEnabled_ShouldHandleResetPassword(Guid orgId, Guid orgUserId,
|
||||||
OrganizationUserAcceptRequestModel model, User user, SutProvider<OrganizationUsersController> sutProvider)
|
OrganizationUserAcceptRequestModel model, User user,
|
||||||
|
[Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,
|
||||||
|
SutProvider<OrganizationUsersController> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
|
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
|
||||||
applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = true });
|
applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = true });
|
||||||
|
|
||||||
var policy = new Policy
|
policy.Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, });
|
||||||
{
|
|
||||||
Enabled = true,
|
|
||||||
Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }),
|
|
||||||
};
|
|
||||||
var userService = sutProvider.GetDependency<IUserService>();
|
var userService = sutProvider.GetDependency<IUserService>();
|
||||||
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||||
|
|
||||||
|
var policyQuery = sutProvider.GetDependency<IPolicyQuery>();
|
||||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
policyQuery.RunAsync(orgId,
|
||||||
policyRepository.GetByOrganizationIdTypeAsync(orgId,
|
|
||||||
PolicyType.ResetPassword).Returns(policy);
|
PolicyType.ResetPassword).Returns(policy);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@ -167,29 +164,27 @@ public class OrganizationUsersControllerTests
|
|||||||
|
|
||||||
await userService.Received(1).GetUserByPrincipalAsync(default);
|
await userService.Received(1).GetUserByPrincipalAsync(default);
|
||||||
await applicationCacheService.Received(1).GetOrganizationAbilityAsync(orgId);
|
await applicationCacheService.Received(1).GetOrganizationAbilityAsync(orgId);
|
||||||
await policyRepository.Received(1).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
await policyQuery.Received(1).RunAsync(orgId, PolicyType.ResetPassword);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task Accept_WhenOrganizationUsePoliciesIsDisabled_ShouldNotHandleResetPassword(Guid orgId, Guid orgUserId,
|
public async Task Accept_WhenOrganizationUsePoliciesIsDisabled_ShouldNotHandleResetPassword(Guid orgId, Guid orgUserId,
|
||||||
OrganizationUserAcceptRequestModel model, User user, SutProvider<OrganizationUsersController> sutProvider)
|
OrganizationUserAcceptRequestModel model, User user,
|
||||||
|
[Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,
|
||||||
|
SutProvider<OrganizationUsersController> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
|
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
|
||||||
applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = false });
|
applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = false });
|
||||||
|
|
||||||
var policy = new Policy
|
policy.Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, });
|
||||||
{
|
|
||||||
Enabled = true,
|
|
||||||
Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }),
|
|
||||||
};
|
|
||||||
var userService = sutProvider.GetDependency<IUserService>();
|
var userService = sutProvider.GetDependency<IUserService>();
|
||||||
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||||
|
|
||||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
var policyQuery = sutProvider.GetDependency<IPolicyQuery>();
|
||||||
policyRepository.GetByOrganizationIdTypeAsync(orgId,
|
policyQuery.RunAsync(orgId,
|
||||||
PolicyType.ResetPassword).Returns(policy);
|
PolicyType.ResetPassword).Returns(policy);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@ -202,7 +197,7 @@ public class OrganizationUsersControllerTests
|
|||||||
await sutProvider.GetDependency<IOrganizationService>().Received(0)
|
await sutProvider.GetDependency<IOrganizationService>().Received(0)
|
||||||
.UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);
|
.UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);
|
||||||
|
|
||||||
await policyRepository.Received(0).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
await policyQuery.Received(0).RunAsync(orgId, PolicyType.ResetPassword);
|
||||||
await applicationCacheService.Received(1).GetOrganizationAbilityAsync(orgId);
|
await applicationCacheService.Received(1).GetOrganizationAbilityAsync(orgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,7 +378,7 @@ public class OrganizationUsersControllerTests
|
|||||||
|
|
||||||
var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
|
var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
|
||||||
|
|
||||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
var policyQuery = sutProvider.GetDependency<IPolicyQuery>();
|
||||||
|
|
||||||
var policyRequirement = new ResetPasswordPolicyRequirement { AutoEnrollOrganizations = [orgId] };
|
var policyRequirement = new ResetPasswordPolicyRequirement { AutoEnrollOrganizations = [orgId] };
|
||||||
|
|
||||||
@ -400,7 +395,7 @@ public class OrganizationUsersControllerTests
|
|||||||
|
|
||||||
await userService.Received(1).GetUserByPrincipalAsync(default);
|
await userService.Received(1).GetUserByPrincipalAsync(default);
|
||||||
await applicationCacheService.Received(0).GetOrganizationAbilityAsync(orgId);
|
await applicationCacheService.Received(0).GetOrganizationAbilityAsync(orgId);
|
||||||
await policyRepository.Received(0).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
await policyQuery.Received(0).RunAsync(orgId, PolicyType.ResetPassword);
|
||||||
await policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
await policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||||
Assert.True(policyRequirement.AutoEnrollEnabled(orgId));
|
Assert.True(policyRequirement.AutoEnrollEnabled(orgId));
|
||||||
}
|
}
|
||||||
@ -425,7 +420,7 @@ public class OrganizationUsersControllerTests
|
|||||||
var userService = sutProvider.GetDependency<IUserService>();
|
var userService = sutProvider.GetDependency<IUserService>();
|
||||||
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||||
|
|
||||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
var policyQuery = sutProvider.GetDependency<IPolicyQuery>();
|
||||||
|
|
||||||
var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
|
var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
|
||||||
|
|
||||||
@ -445,7 +440,7 @@ public class OrganizationUsersControllerTests
|
|||||||
|
|
||||||
await userService.Received(1).GetUserByPrincipalAsync(default);
|
await userService.Received(1).GetUserByPrincipalAsync(default);
|
||||||
await applicationCacheService.Received(0).GetOrganizationAbilityAsync(orgId);
|
await applicationCacheService.Received(0).GetOrganizationAbilityAsync(orgId);
|
||||||
await policyRepository.Received(0).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
await policyQuery.Received(0).RunAsync(orgId, PolicyType.ResetPassword);
|
||||||
await policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
await policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||||
|
|
||||||
Assert.Equal("Master Password reset is required, but not provided.", exception.Message);
|
Assert.Equal("Master Password reset is required, but not provided.", exception.Message);
|
||||||
|
|||||||
@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Entities;
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Business;
|
using Bit.Core.AdminConsole.Models.Business;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
@ -25,6 +26,7 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||||
using Bit.Core.Test.Billing.Mocks;
|
using Bit.Core.Test.Billing.Mocks;
|
||||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
@ -200,28 +202,21 @@ public class OrganizationsControllerTests
|
|||||||
SutProvider<OrganizationsController> sutProvider,
|
SutProvider<OrganizationsController> sutProvider,
|
||||||
User user,
|
User user,
|
||||||
Organization organization,
|
Organization organization,
|
||||||
OrganizationUser organizationUser)
|
OrganizationUser organizationUser,
|
||||||
|
[Policy(PolicyType.ResetPassword, data: "{\"AutoEnrollEnabled\": true}")] PolicyStatus policy)
|
||||||
{
|
{
|
||||||
var policy = new Policy
|
|
||||||
{
|
|
||||||
Type = PolicyType.ResetPassword,
|
|
||||||
Enabled = true,
|
|
||||||
Data = "{\"AutoEnrollEnabled\": true}",
|
|
||||||
OrganizationId = organization.Id
|
|
||||||
};
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(organization.Id.ToString()).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(organization.Id.ToString()).Returns(organization);
|
||||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(false);
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(false);
|
||||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(organization.Id, user.Id).Returns(organizationUser);
|
sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(organization.Id, user.Id).Returns(organizationUser);
|
||||||
sutProvider.GetDependency<IPolicyRepository>().GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword).Returns(policy);
|
sutProvider.GetDependency<IPolicyQuery>().RunAsync(organization.Id, PolicyType.ResetPassword).Returns(policy);
|
||||||
|
|
||||||
var result = await sutProvider.Sut.GetAutoEnrollStatus(organization.Id.ToString());
|
var result = await sutProvider.Sut.GetAutoEnrollStatus(organization.Id.ToString());
|
||||||
|
|
||||||
await sutProvider.GetDependency<IUserService>().Received(1).GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>());
|
await sutProvider.GetDependency<IUserService>().Received(1).GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>());
|
||||||
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetByIdentifierAsync(organization.Id.ToString());
|
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetByIdentifierAsync(organization.Id.ToString());
|
||||||
await sutProvider.GetDependency<IPolicyRequirementQuery>().Received(0).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
await sutProvider.GetDependency<IPolicyRequirementQuery>().Received(0).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||||
await sutProvider.GetDependency<IPolicyRepository>().Received(1).GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
|
await sutProvider.GetDependency<IPolicyQuery>().Received(1).RunAsync(organization.Id, PolicyType.ResetPassword);
|
||||||
|
|
||||||
Assert.True(result.ResetPasswordEnabled);
|
Assert.True(result.ResetPasswordEnabled);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
using AutoFixture;
|
using Bit.Api.AdminConsole.Models.Response.Helpers;
|
||||||
using Bit.Api.AdminConsole.Models.Response.Helpers;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Api.Test.AdminConsole.Models.Response.Helpers;
|
namespace Bit.Api.Test.AdminConsole.Models.Response.Helpers;
|
||||||
|
|
||||||
public class PolicyDetailResponsesTests
|
public class PolicyStatusResponsesTests
|
||||||
{
|
{
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(true, false)]
|
[InlineData(true, false)]
|
||||||
@ -17,19 +16,13 @@ public class PolicyDetailResponsesTests
|
|||||||
bool policyEnabled,
|
bool policyEnabled,
|
||||||
bool expectedCanToggle)
|
bool expectedCanToggle)
|
||||||
{
|
{
|
||||||
var fixture = new Fixture();
|
var policy = new PolicyStatus(Guid.NewGuid(), PolicyType.SingleOrg) { Enabled = policyEnabled };
|
||||||
|
|
||||||
var policy = fixture.Build<Policy>()
|
|
||||||
.Without(p => p.Data)
|
|
||||||
.With(p => p.Type, PolicyType.SingleOrg)
|
|
||||||
.With(p => p.Enabled, policyEnabled)
|
|
||||||
.Create();
|
|
||||||
|
|
||||||
var querySub = Substitute.For<IOrganizationHasVerifiedDomainsQuery>();
|
var querySub = Substitute.For<IOrganizationHasVerifiedDomainsQuery>();
|
||||||
querySub.HasVerifiedDomainsAsync(policy.OrganizationId)
|
querySub.HasVerifiedDomainsAsync(policy.OrganizationId)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|
||||||
var result = await policy.GetSingleOrgPolicyDetailResponseAsync(querySub);
|
var result = await policy.GetSingleOrgPolicyStatusResponseAsync(querySub);
|
||||||
|
|
||||||
Assert.Equal(expectedCanToggle, result.CanToggleState);
|
Assert.Equal(expectedCanToggle, result.CanToggleState);
|
||||||
}
|
}
|
||||||
@ -37,18 +30,13 @@ public class PolicyDetailResponsesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetSingleOrgPolicyDetailResponseAsync_WhenIsNotSingleOrgType_ThenShouldThrowArgumentException()
|
public async Task GetSingleOrgPolicyDetailResponseAsync_WhenIsNotSingleOrgType_ThenShouldThrowArgumentException()
|
||||||
{
|
{
|
||||||
var fixture = new Fixture();
|
var policy = new PolicyStatus(Guid.NewGuid(), PolicyType.TwoFactorAuthentication);
|
||||||
|
|
||||||
var policy = fixture.Build<Policy>()
|
|
||||||
.Without(p => p.Data)
|
|
||||||
.With(p => p.Type, PolicyType.TwoFactorAuthentication)
|
|
||||||
.Create();
|
|
||||||
|
|
||||||
var querySub = Substitute.For<IOrganizationHasVerifiedDomainsQuery>();
|
var querySub = Substitute.For<IOrganizationHasVerifiedDomainsQuery>();
|
||||||
querySub.HasVerifiedDomainsAsync(policy.OrganizationId)
|
querySub.HasVerifiedDomainsAsync(policy.OrganizationId)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|
||||||
var action = async () => await policy.GetSingleOrgPolicyDetailResponseAsync(querySub);
|
var action = async () => await policy.GetSingleOrgPolicyStatusResponseAsync(querySub);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<ArgumentException>("policy", action);
|
await Assert.ThrowsAsync<ArgumentException>("policy", action);
|
||||||
}
|
}
|
||||||
@ -56,18 +44,13 @@ public class PolicyDetailResponsesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetSingleOrgPolicyDetailResponseAsync_WhenIsSingleOrgTypeAndDoesNotHaveVerifiedDomains_ThenShouldBeAbleToToggle()
|
public async Task GetSingleOrgPolicyDetailResponseAsync_WhenIsSingleOrgTypeAndDoesNotHaveVerifiedDomains_ThenShouldBeAbleToToggle()
|
||||||
{
|
{
|
||||||
var fixture = new Fixture();
|
var policy = new PolicyStatus(Guid.NewGuid(), PolicyType.SingleOrg);
|
||||||
|
|
||||||
var policy = fixture.Build<Policy>()
|
|
||||||
.Without(p => p.Data)
|
|
||||||
.With(p => p.Type, PolicyType.SingleOrg)
|
|
||||||
.Create();
|
|
||||||
|
|
||||||
var querySub = Substitute.For<IOrganizationHasVerifiedDomainsQuery>();
|
var querySub = Substitute.For<IOrganizationHasVerifiedDomainsQuery>();
|
||||||
querySub.HasVerifiedDomainsAsync(policy.OrganizationId)
|
querySub.HasVerifiedDomainsAsync(policy.OrganizationId)
|
||||||
.Returns(false);
|
.Returns(false);
|
||||||
|
|
||||||
var result = await policy.GetSingleOrgPolicyDetailResponseAsync(querySub);
|
var result = await policy.GetSingleOrgPolicyStatusResponseAsync(querySub);
|
||||||
|
|
||||||
Assert.True(result.CanToggleState);
|
Assert.True(result.CanToggleState);
|
||||||
}
|
}
|
||||||
@ -1,6 +1,9 @@
|
|||||||
using Bit.Api.Billing.Controllers;
|
using Bit.Api.Billing.Controllers;
|
||||||
using Bit.Api.Models.Request.Organizations;
|
using Bit.Api.Models.Request.Organizations;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
@ -10,6 +13,7 @@ using Bit.Core.Models.Data;
|
|||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||||
using Bit.Core.Test.Billing.Mocks;
|
using Bit.Core.Test.Billing.Mocks;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
@ -82,7 +86,9 @@ public class OrganizationSponsorshipsControllerTests
|
|||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task RedeemSponsorship_NotSponsoredOrgOwner_Success(string sponsorshipToken, User user,
|
public async Task RedeemSponsorship_NotSponsoredOrgOwner_Success(string sponsorshipToken, User user,
|
||||||
OrganizationSponsorship sponsorship, Organization sponsoringOrganization,
|
OrganizationSponsorship sponsorship, Organization sponsoringOrganization,
|
||||||
OrganizationSponsorshipRedeemRequestModel model, SutProvider<OrganizationSponsorshipsController> sutProvider)
|
OrganizationSponsorshipRedeemRequestModel model,
|
||||||
|
[Policy(PolicyType.FreeFamiliesSponsorshipPolicy, false)] PolicyStatus policy,
|
||||||
|
SutProvider<OrganizationSponsorshipsController> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(user.Id);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(user.Id);
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(user.Id)
|
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(user.Id)
|
||||||
@ -91,6 +97,9 @@ public class OrganizationSponsorshipsControllerTests
|
|||||||
user.Email).Returns((true, sponsorship));
|
user.Email).Returns((true, sponsorship));
|
||||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(model.SponsoredOrganizationId).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(model.SponsoredOrganizationId).Returns(true);
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(model.SponsoredOrganizationId).Returns(sponsoringOrganization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(model.SponsoredOrganizationId).Returns(sponsoringOrganization);
|
||||||
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
|
.RunAsync(Arg.Any<Guid>(), PolicyType.FreeFamiliesSponsorshipPolicy)
|
||||||
|
.Returns(policy);
|
||||||
|
|
||||||
await sutProvider.Sut.RedeemSponsorship(sponsorshipToken, model);
|
await sutProvider.Sut.RedeemSponsorship(sponsorshipToken, model);
|
||||||
|
|
||||||
@ -101,14 +110,18 @@ public class OrganizationSponsorshipsControllerTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task PreValidateSponsorshipToken_ValidatesToken_Success(string sponsorshipToken, User user,
|
public async Task PreValidateSponsorshipToken_ValidatesToken_Success(string sponsorshipToken, User user,
|
||||||
OrganizationSponsorship sponsorship, SutProvider<OrganizationSponsorshipsController> sutProvider)
|
OrganizationSponsorship sponsorship,
|
||||||
|
[Policy(PolicyType.FreeFamiliesSponsorshipPolicy, false)] PolicyStatus policy,
|
||||||
|
SutProvider<OrganizationSponsorshipsController> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(user.Id);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(user.Id);
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(user.Id)
|
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(user.Id)
|
||||||
.Returns(user);
|
.Returns(user);
|
||||||
sutProvider.GetDependency<IValidateRedemptionTokenCommand>()
|
sutProvider.GetDependency<IValidateRedemptionTokenCommand>()
|
||||||
.ValidateRedemptionTokenAsync(sponsorshipToken, user.Email).Returns((true, sponsorship));
|
.ValidateRedemptionTokenAsync(sponsorshipToken, user.Email).Returns((true, sponsorship));
|
||||||
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
|
.RunAsync(Arg.Any<Guid>(), PolicyType.FreeFamiliesSponsorshipPolicy)
|
||||||
|
.Returns(policy);
|
||||||
await sutProvider.Sut.PreValidateSponsorshipToken(sponsorshipToken);
|
await sutProvider.Sut.PreValidateSponsorshipToken(sponsorshipToken);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IValidateRedemptionTokenCommand>().Received(1)
|
await sutProvider.GetDependency<IValidateRedemptionTokenCommand>().Received(1)
|
||||||
|
|||||||
@ -0,0 +1,56 @@
|
|||||||
|
using Bit.Api.Billing.Models.Requests.Payment;
|
||||||
|
using Bit.Api.Billing.Models.Requests.PreviewInvoice;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Api.Test.Billing.Models.Requests;
|
||||||
|
|
||||||
|
public class PreviewPremiumUpgradeProrationRequestTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(ProductTierType.Families, PlanType.FamiliesAnnually)]
|
||||||
|
[InlineData(ProductTierType.Teams, PlanType.TeamsAnnually)]
|
||||||
|
[InlineData(ProductTierType.Enterprise, PlanType.EnterpriseAnnually)]
|
||||||
|
public void ToDomain_ValidTierTypes_ReturnsPlanType(ProductTierType tierType, PlanType expectedPlanType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sut = new PreviewPremiumUpgradeProrationRequest
|
||||||
|
{
|
||||||
|
TargetProductTierType = tierType,
|
||||||
|
BillingAddress = new MinimalBillingAddressRequest
|
||||||
|
{
|
||||||
|
Country = "US",
|
||||||
|
PostalCode = "12345"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var (planType, billingAddress) = sut.ToDomain();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedPlanType, planType);
|
||||||
|
Assert.Equal("US", billingAddress.Country);
|
||||||
|
Assert.Equal("12345", billingAddress.PostalCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(ProductTierType.Free)]
|
||||||
|
[InlineData(ProductTierType.TeamsStarter)]
|
||||||
|
public void ToDomain_InvalidTierTypes_ThrowsInvalidOperationException(ProductTierType tierType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sut = new PreviewPremiumUpgradeProrationRequest
|
||||||
|
{
|
||||||
|
TargetProductTierType = tierType,
|
||||||
|
BillingAddress = new MinimalBillingAddressRequest
|
||||||
|
{
|
||||||
|
Country = "US",
|
||||||
|
PostalCode = "12345"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = Assert.Throws<InvalidOperationException>(() => sut.ToDomain());
|
||||||
|
Assert.Contains($"Cannot upgrade Premium subscription to {tierType} plan", exception.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
using Bit.Api.Billing.Models.Requests.Payment;
|
||||||
|
using Bit.Api.Billing.Models.Requests.Premium;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Api.Test.Billing.Models.Requests;
|
||||||
|
|
||||||
|
public class UpgradePremiumToOrganizationRequestTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(ProductTierType.Families, PlanType.FamiliesAnnually)]
|
||||||
|
[InlineData(ProductTierType.Teams, PlanType.TeamsAnnually)]
|
||||||
|
[InlineData(ProductTierType.Enterprise, PlanType.EnterpriseAnnually)]
|
||||||
|
public void ToDomain_ValidTierTypes_ReturnsPlanType(ProductTierType tierType, PlanType expectedPlanType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sut = new UpgradePremiumToOrganizationRequest
|
||||||
|
{
|
||||||
|
OrganizationName = "Test Organization",
|
||||||
|
Key = "encrypted-key",
|
||||||
|
TargetProductTierType = tierType,
|
||||||
|
BillingAddress = new MinimalBillingAddressRequest
|
||||||
|
{
|
||||||
|
Country = "US",
|
||||||
|
PostalCode = "12345"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var (organizationName, key, planType, billingAddress) = sut.ToDomain();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Test Organization", organizationName);
|
||||||
|
Assert.Equal("encrypted-key", key);
|
||||||
|
Assert.Equal(expectedPlanType, planType);
|
||||||
|
Assert.Equal("US", billingAddress.Country);
|
||||||
|
Assert.Equal("12345", billingAddress.PostalCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(ProductTierType.Free)]
|
||||||
|
[InlineData(ProductTierType.TeamsStarter)]
|
||||||
|
public void ToDomain_InvalidTierTypes_ThrowsInvalidOperationException(ProductTierType tierType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sut = new UpgradePremiumToOrganizationRequest
|
||||||
|
{
|
||||||
|
OrganizationName = "Test Organization",
|
||||||
|
Key = "encrypted-key",
|
||||||
|
TargetProductTierType = tierType,
|
||||||
|
BillingAddress = new MinimalBillingAddressRequest
|
||||||
|
{
|
||||||
|
Country = "US",
|
||||||
|
PostalCode = "12345"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = Assert.Throws<InvalidOperationException>(() => sut.ToDomain());
|
||||||
|
Assert.Contains($"Cannot upgrade Premium subscription to {tierType} plan", exception.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -49,7 +49,7 @@ public class PoliciesControllerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<IUserService>()
|
sutProvider.GetDependency<IUserService>()
|
||||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||||
.Returns((Guid?)userId);
|
.Returns(userId);
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
.GetByOrganizationAsync(orgId, userId)
|
.GetByOrganizationAsync(orgId, userId)
|
||||||
@ -95,7 +95,7 @@ public class PoliciesControllerTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
sutProvider.GetDependency<IUserService>()
|
sutProvider.GetDependency<IUserService>()
|
||||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||||
.Returns((Guid?)userId);
|
.Returns(userId);
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
.GetByOrganizationAsync(orgId, userId)
|
.GetByOrganizationAsync(orgId, userId)
|
||||||
@ -113,7 +113,7 @@ public class PoliciesControllerTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
sutProvider.GetDependency<IUserService>()
|
sutProvider.GetDependency<IUserService>()
|
||||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||||
.Returns((Guid?)userId);
|
.Returns(userId);
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
.GetByOrganizationAsync(orgId, userId)
|
.GetByOrganizationAsync(orgId, userId)
|
||||||
@ -135,7 +135,7 @@ public class PoliciesControllerTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
sutProvider.GetDependency<IUserService>()
|
sutProvider.GetDependency<IUserService>()
|
||||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||||
.Returns((Guid?)userId);
|
.Returns(userId);
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
.GetByOrganizationAsync(orgId, userId)
|
.GetByOrganizationAsync(orgId, userId)
|
||||||
@ -186,59 +186,35 @@ public class PoliciesControllerTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task Get_WhenUserCanManagePolicies_WithExistingType_ReturnsExistingPolicy(
|
public async Task Get_WhenUserCanManagePolicies_WithExistingType_ReturnsExistingPolicy(
|
||||||
SutProvider<PoliciesController> sutProvider, Guid orgId, Policy policy, int type)
|
SutProvider<PoliciesController> sutProvider, Guid orgId, PolicyStatus policy, PolicyType type)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
.ManagePolicies(orgId)
|
.ManagePolicies(orgId)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|
||||||
policy.Type = (PolicyType)type;
|
policy.Type = type;
|
||||||
policy.Enabled = true;
|
policy.Enabled = true;
|
||||||
policy.Data = null;
|
policy.Data = null;
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyRepository>()
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type)
|
.RunAsync(orgId, type)
|
||||||
.Returns(policy);
|
.Returns(policy);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await sutProvider.Sut.Get(orgId, type);
|
var result = await sutProvider.Sut.Get(orgId, type);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsType<PolicyDetailResponseModel>(result);
|
Assert.IsType<PolicyStatusResponseModel>(result);
|
||||||
Assert.Equal(policy.Id, result.Id);
|
|
||||||
Assert.Equal(policy.Type, result.Type);
|
Assert.Equal(policy.Type, result.Type);
|
||||||
Assert.Equal(policy.Enabled, result.Enabled);
|
Assert.Equal(policy.Enabled, result.Enabled);
|
||||||
Assert.Equal(policy.OrganizationId, result.OrganizationId);
|
Assert.Equal(policy.OrganizationId, result.OrganizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task Get_WhenUserCanManagePolicies_WithNonExistingType_ReturnsDefaultPolicy(
|
|
||||||
SutProvider<PoliciesController> sutProvider, Guid orgId, int type)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
|
||||||
.ManagePolicies(orgId)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyRepository>()
|
|
||||||
.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type)
|
|
||||||
.Returns((Policy)null);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await sutProvider.Sut.Get(orgId, type);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.IsType<PolicyDetailResponseModel>(result);
|
|
||||||
Assert.Equal(result.Type, (PolicyType)type);
|
|
||||||
Assert.False(result.Enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task Get_WhenUserCannotManagePolicies_ThrowsNotFoundException(
|
public async Task Get_WhenUserCannotManagePolicies_ThrowsNotFoundException(
|
||||||
SutProvider<PoliciesController> sutProvider, Guid orgId, int type)
|
SutProvider<PoliciesController> sutProvider, Guid orgId, PolicyType type)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
|
|||||||
@ -806,63 +806,6 @@ public class ImportCiphersControllerTests
|
|||||||
Arg.Any<Guid>());
|
Arg.Any<Guid>());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task PostImportOrganization_ThrowsException_WhenAnyCipherIsArchived(
|
|
||||||
SutProvider<ImportCiphersController> sutProvider,
|
|
||||||
IFixture fixture,
|
|
||||||
User user
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var orgId = Guid.NewGuid();
|
|
||||||
|
|
||||||
sutProvider.GetDependency<GlobalSettings>()
|
|
||||||
.SelfHosted = false;
|
|
||||||
sutProvider.GetDependency<GlobalSettings>()
|
|
||||||
.ImportCiphersLimitation = _organizationCiphersLimitations;
|
|
||||||
|
|
||||||
SetupUserService(sutProvider, user);
|
|
||||||
|
|
||||||
var ciphers = fixture.Build<CipherRequestModel>()
|
|
||||||
.With(_ => _.ArchivedDate, DateTime.UtcNow)
|
|
||||||
.CreateMany(2).ToArray();
|
|
||||||
|
|
||||||
var request = new ImportOrganizationCiphersRequestModel
|
|
||||||
{
|
|
||||||
Collections = new List<CollectionWithIdRequestModel>().ToArray(),
|
|
||||||
Ciphers = ciphers,
|
|
||||||
CollectionRelationships = new List<KeyValuePair<int, int>>().ToArray(),
|
|
||||||
};
|
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
|
||||||
.AccessImportExport(Arg.Any<Guid>())
|
|
||||||
.Returns(false);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IAuthorizationService>()
|
|
||||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
|
|
||||||
Arg.Any<IEnumerable<Collection>>(),
|
|
||||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>
|
|
||||||
reqs.Contains(BulkCollectionOperations.ImportCiphers)))
|
|
||||||
.Returns(AuthorizationResult.Failed());
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IAuthorizationService>()
|
|
||||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
|
|
||||||
Arg.Any<IEnumerable<Collection>>(),
|
|
||||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>
|
|
||||||
reqs.Contains(BulkCollectionOperations.Create)))
|
|
||||||
.Returns(AuthorizationResult.Success());
|
|
||||||
|
|
||||||
sutProvider.GetDependency<ICollectionRepository>()
|
|
||||||
.GetManyByOrganizationIdAsync(orgId)
|
|
||||||
.Returns(new List<Collection>());
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
|
||||||
{
|
|
||||||
await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request);
|
|
||||||
});
|
|
||||||
|
|
||||||
Assert.Equal("You cannot import archived items into an organization.", exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SetupUserService(SutProvider<ImportCiphersController> sutProvider, User user)
|
private static void SetupUserService(SutProvider<ImportCiphersController> sutProvider, User user)
|
||||||
{
|
{
|
||||||
// This is a workaround for the NSubstitute issue with ambiguous arguments
|
// This is a workaround for the NSubstitute issue with ambiguous arguments
|
||||||
|
|||||||
@ -819,7 +819,7 @@ public class SubscriptionUpdatedHandlerTests
|
|||||||
{
|
{
|
||||||
Data =
|
Data =
|
||||||
[
|
[
|
||||||
new SubscriptionItem { Plan = new Plan { Id = "secrets-manager-teams-seat-annually" } },
|
new SubscriptionItem { Plan = new Stripe.Plan { Id = "secrets-manager-teams-seat-annually" } },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
17
test/Common/Helpers/CryptographyHelper.cs
Normal file
17
test/Common/Helpers/CryptographyHelper.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Bit.Test.Common.Helpers;
|
||||||
|
|
||||||
|
public class CryptographyHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a hex-encoded, SHA256 hash for the given string
|
||||||
|
/// </summary>
|
||||||
|
public static string HashAndEncode(string text)
|
||||||
|
{
|
||||||
|
var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(text));
|
||||||
|
var hashEncoded = Convert.ToHexString(hashBytes).ToUpperInvariant();
|
||||||
|
return hashEncoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ using AutoFixture;
|
|||||||
using AutoFixture.Xunit2;
|
using AutoFixture.Xunit2;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
|
||||||
namespace Bit.Core.Test.AdminConsole.AutoFixture;
|
namespace Bit.Core.Test.AdminConsole.AutoFixture;
|
||||||
|
|
||||||
@ -10,19 +11,30 @@ internal class PolicyCustomization : ICustomization
|
|||||||
{
|
{
|
||||||
public PolicyType Type { get; set; }
|
public PolicyType Type { get; set; }
|
||||||
public bool Enabled { get; set; }
|
public bool Enabled { get; set; }
|
||||||
|
public string? Data { get; set; }
|
||||||
|
|
||||||
public PolicyCustomization(PolicyType type, bool enabled)
|
public PolicyCustomization(PolicyType type, bool enabled, string? data)
|
||||||
{
|
{
|
||||||
Type = type;
|
Type = type;
|
||||||
Enabled = enabled;
|
Enabled = enabled;
|
||||||
|
Data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Customize(IFixture fixture)
|
public void Customize(IFixture fixture)
|
||||||
{
|
{
|
||||||
|
var orgId = Guid.NewGuid();
|
||||||
|
|
||||||
fixture.Customize<Policy>(composer => composer
|
fixture.Customize<Policy>(composer => composer
|
||||||
.With(o => o.OrganizationId, Guid.NewGuid())
|
.With(o => o.OrganizationId, orgId)
|
||||||
.With(o => o.Type, Type)
|
.With(o => o.Type, Type)
|
||||||
.With(o => o.Enabled, Enabled));
|
.With(o => o.Enabled, Enabled)
|
||||||
|
.With(o => o.Data, Data));
|
||||||
|
|
||||||
|
fixture.Customize<PolicyStatus>(composer => composer
|
||||||
|
.With(o => o.OrganizationId, orgId)
|
||||||
|
.With(o => o.Type, Type)
|
||||||
|
.With(o => o.Enabled, Enabled)
|
||||||
|
.With(o => o.Data, Data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,15 +42,17 @@ public class PolicyAttribute : CustomizeAttribute
|
|||||||
{
|
{
|
||||||
private readonly PolicyType _type;
|
private readonly PolicyType _type;
|
||||||
private readonly bool _enabled;
|
private readonly bool _enabled;
|
||||||
|
private readonly string? _data;
|
||||||
|
|
||||||
public PolicyAttribute(PolicyType type, bool enabled = true)
|
public PolicyAttribute(PolicyType type, bool enabled = true, string? data = null)
|
||||||
{
|
{
|
||||||
_type = type;
|
_type = type;
|
||||||
_enabled = enabled;
|
_enabled = enabled;
|
||||||
|
_data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override ICustomization GetCustomization(ParameterInfo parameter)
|
public override ICustomization GetCustomization(ParameterInfo parameter)
|
||||||
{
|
{
|
||||||
return new PolicyCustomization(_type, _enabled);
|
return new PolicyCustomization(_type, _enabled, _data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
using AutoFixture;
|
using AutoFixture;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
|
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Platform.Push;
|
using Bit.Core.Platform.Push;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
@ -29,11 +31,12 @@ public class AdminRecoverAccountCommandTests
|
|||||||
Organization organization,
|
Organization organization,
|
||||||
OrganizationUser organizationUser,
|
OrganizationUser organizationUser,
|
||||||
User user,
|
User user,
|
||||||
|
[Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,
|
||||||
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
SetupValidOrganization(sutProvider, organization);
|
SetupValidOrganization(sutProvider, organization);
|
||||||
SetupValidPolicy(sutProvider, organization);
|
SetupValidPolicy(sutProvider, organization, policy);
|
||||||
SetupValidOrganizationUser(organizationUser, organization.Id);
|
SetupValidOrganizationUser(organizationUser, organization.Id);
|
||||||
SetupValidUser(sutProvider, user, organizationUser);
|
SetupValidUser(sutProvider, user, organizationUser);
|
||||||
SetupSuccessfulPasswordUpdate(sutProvider, user, newMasterPassword);
|
SetupSuccessfulPasswordUpdate(sutProvider, user, newMasterPassword);
|
||||||
@ -87,25 +90,18 @@ public class AdminRecoverAccountCommandTests
|
|||||||
Assert.Equal("Organization does not allow password reset.", exception.Message);
|
Assert.Equal("Organization does not allow password reset.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<object[]> InvalidPolicies => new object[][]
|
|
||||||
{
|
|
||||||
[new Policy { Type = PolicyType.ResetPassword, Enabled = false }], [null]
|
|
||||||
};
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitMemberAutoData(nameof(InvalidPolicies))]
|
[BitAutoData]
|
||||||
public async Task RecoverAccountAsync_InvalidPolicy_ThrowsBadRequest(
|
public async Task RecoverAccountAsync_InvalidPolicy_ThrowsBadRequest(
|
||||||
Policy resetPasswordPolicy,
|
|
||||||
string newMasterPassword,
|
string newMasterPassword,
|
||||||
string key,
|
string key,
|
||||||
Organization organization,
|
Organization organization,
|
||||||
|
[Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,
|
||||||
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
SetupValidOrganization(sutProvider, organization);
|
SetupValidOrganization(sutProvider, organization);
|
||||||
sutProvider.GetDependency<IPolicyRepository>()
|
SetupValidPolicy(sutProvider, organization, policy);
|
||||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword)
|
|
||||||
.Returns(resetPasswordPolicy);
|
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
@ -171,11 +167,12 @@ public class AdminRecoverAccountCommandTests
|
|||||||
Organization organization,
|
Organization organization,
|
||||||
string newMasterPassword,
|
string newMasterPassword,
|
||||||
string key,
|
string key,
|
||||||
|
[Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,
|
||||||
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
SetupValidOrganization(sutProvider, organization);
|
SetupValidOrganization(sutProvider, organization);
|
||||||
SetupValidPolicy(sutProvider, organization);
|
SetupValidPolicy(sutProvider, organization, policy);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
@ -190,11 +187,12 @@ public class AdminRecoverAccountCommandTests
|
|||||||
string key,
|
string key,
|
||||||
Organization organization,
|
Organization organization,
|
||||||
OrganizationUser organizationUser,
|
OrganizationUser organizationUser,
|
||||||
|
[Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,
|
||||||
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
SetupValidOrganization(sutProvider, organization);
|
SetupValidOrganization(sutProvider, organization);
|
||||||
SetupValidPolicy(sutProvider, organization);
|
SetupValidPolicy(sutProvider, organization, policy);
|
||||||
SetupValidOrganizationUser(organizationUser, organization.Id);
|
SetupValidOrganizationUser(organizationUser, organization.Id);
|
||||||
sutProvider.GetDependency<IUserService>()
|
sutProvider.GetDependency<IUserService>()
|
||||||
.GetUserByIdAsync(organizationUser.UserId!.Value)
|
.GetUserByIdAsync(organizationUser.UserId!.Value)
|
||||||
@ -213,11 +211,12 @@ public class AdminRecoverAccountCommandTests
|
|||||||
Organization organization,
|
Organization organization,
|
||||||
OrganizationUser organizationUser,
|
OrganizationUser organizationUser,
|
||||||
User user,
|
User user,
|
||||||
|
[Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,
|
||||||
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
SetupValidOrganization(sutProvider, organization);
|
SetupValidOrganization(sutProvider, organization);
|
||||||
SetupValidPolicy(sutProvider, organization);
|
SetupValidPolicy(sutProvider, organization, policy);
|
||||||
SetupValidOrganizationUser(organizationUser, organization.Id);
|
SetupValidOrganizationUser(organizationUser, organization.Id);
|
||||||
user.UsesKeyConnector = true;
|
user.UsesKeyConnector = true;
|
||||||
sutProvider.GetDependency<IUserService>()
|
sutProvider.GetDependency<IUserService>()
|
||||||
@ -238,11 +237,10 @@ public class AdminRecoverAccountCommandTests
|
|||||||
.Returns(organization);
|
.Returns(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SetupValidPolicy(SutProvider<AdminRecoverAccountCommand> sutProvider, Organization organization)
|
private static void SetupValidPolicy(SutProvider<AdminRecoverAccountCommand> sutProvider, Organization organization, PolicyStatus policy)
|
||||||
{
|
{
|
||||||
var policy = new Policy { Type = PolicyType.ResetPassword, Enabled = true };
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
sutProvider.GetDependency<IPolicyRepository>()
|
.RunAsync(organization.Id, PolicyType.ResetPassword)
|
||||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword)
|
|
||||||
.Returns(policy);
|
.Returns(policy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimed
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
@ -120,7 +119,7 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
|||||||
[Organization(useAutomaticUserConfirmation: true, planType: PlanType.EnterpriseAnnually)] Organization organization,
|
[Organization(useAutomaticUserConfirmation: true, planType: PlanType.EnterpriseAnnually)] Organization organization,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||||
User user,
|
User user,
|
||||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
[Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
organizationUser.UserId = user.Id;
|
organizationUser.UserId = user.Id;
|
||||||
@ -137,8 +136,8 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
|||||||
Key = "test-key"
|
Key = "test-key"
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyRepository>()
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
.RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||||
.Returns(autoConfirmPolicy);
|
.Returns(autoConfirmPolicy);
|
||||||
|
|
||||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||||
@ -280,7 +279,7 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
|||||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||||
Guid userId,
|
Guid userId,
|
||||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
[Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
organizationUser.UserId = userId;
|
organizationUser.UserId = userId;
|
||||||
@ -303,8 +302,8 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
|||||||
PolicyType = PolicyType.TwoFactorAuthentication
|
PolicyType = PolicyType.TwoFactorAuthentication
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyRepository>()
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
.RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||||
.Returns(autoConfirmPolicy);
|
.Returns(autoConfirmPolicy);
|
||||||
|
|
||||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||||
@ -334,7 +333,7 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
|||||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||||
User user,
|
User user,
|
||||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
[Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
organizationUser.UserId = user.Id;
|
organizationUser.UserId = user.Id;
|
||||||
@ -351,8 +350,8 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
|||||||
Key = "test-key"
|
Key = "test-key"
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyRepository>()
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
.RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||||
.Returns(autoConfirmPolicy);
|
.Returns(autoConfirmPolicy);
|
||||||
|
|
||||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||||
@ -389,7 +388,7 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
|||||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||||
User user,
|
User user,
|
||||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
[Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
organizationUser.UserId = user.Id;
|
organizationUser.UserId = user.Id;
|
||||||
@ -406,8 +405,8 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
|||||||
Key = "test-key"
|
Key = "test-key"
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyRepository>()
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
.RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||||
.Returns(autoConfirmPolicy);
|
.Returns(autoConfirmPolicy);
|
||||||
|
|
||||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||||
@ -448,7 +447,7 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
|||||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||||
User user,
|
User user,
|
||||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
[Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
organizationUser.UserId = user.Id;
|
organizationUser.UserId = user.Id;
|
||||||
@ -465,8 +464,8 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
|||||||
Key = "test-key"
|
Key = "test-key"
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyRepository>()
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
.RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||||
.Returns(autoConfirmPolicy);
|
.Returns(autoConfirmPolicy);
|
||||||
|
|
||||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||||
@ -501,7 +500,8 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
|||||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||||
Organization organization,
|
Organization organization,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||||
Guid userId)
|
Guid userId,
|
||||||
|
[Policy(PolicyType.AutomaticUserConfirmation, false)] PolicyStatus policy)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
organizationUser.UserId = userId;
|
organizationUser.UserId = userId;
|
||||||
@ -518,9 +518,9 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
|||||||
Key = "test-key"
|
Key = "test-key"
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyRepository>()
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
.RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||||
.Returns((Policy)null);
|
.Returns(policy);
|
||||||
|
|
||||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||||
@ -545,7 +545,7 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
|||||||
[Organization(useAutomaticUserConfirmation: false)] Organization organization,
|
[Organization(useAutomaticUserConfirmation: false)] Organization organization,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||||
Guid userId,
|
Guid userId,
|
||||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
[Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
organizationUser.UserId = userId;
|
organizationUser.UserId = userId;
|
||||||
@ -562,8 +562,8 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
|||||||
Key = "test-key"
|
Key = "test-key"
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyRepository>()
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
.RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||||
.Returns(autoConfirmPolicy);
|
.Returns(autoConfirmPolicy);
|
||||||
|
|
||||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||||
@ -589,7 +589,7 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
|||||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||||
User user,
|
User user,
|
||||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
[Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
organizationUser.UserId = user.Id;
|
organizationUser.UserId = user.Id;
|
||||||
@ -606,8 +606,8 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
|||||||
Key = "test-key"
|
Key = "test-key"
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyRepository>()
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
.RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||||
.Returns(autoConfirmPolicy);
|
.Returns(autoConfirmPolicy);
|
||||||
|
|
||||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
@ -9,6 +11,7 @@ using Bit.Core.Billing.Enums;
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Models.Mail;
|
using Bit.Core.Models.Mail;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
@ -31,6 +34,7 @@ public class SendOrganizationInvitesCommandTests
|
|||||||
Organization organization,
|
Organization organization,
|
||||||
SsoConfig ssoConfig,
|
SsoConfig ssoConfig,
|
||||||
OrganizationUser invite,
|
OrganizationUser invite,
|
||||||
|
[Policy(PolicyType.RequireSso, false)] PolicyStatus policy,
|
||||||
SutProvider<SendOrganizationInvitesCommand> sutProvider)
|
SutProvider<SendOrganizationInvitesCommand> sutProvider)
|
||||||
{
|
{
|
||||||
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
|
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
|
||||||
@ -45,7 +49,9 @@ public class SendOrganizationInvitesCommandTests
|
|||||||
sutProvider.GetDependency<ISsoConfigRepository>().GetByOrganizationIdAsync(organization.Id).Returns(ssoConfig);
|
sutProvider.GetDependency<ISsoConfigRepository>().GetByOrganizationIdAsync(organization.Id).Returns(ssoConfig);
|
||||||
|
|
||||||
// Return null policy to mimic new org that's never turned on the require sso policy
|
// Return null policy to mimic new org that's never turned on the require sso policy
|
||||||
sutProvider.GetDependency<IPolicyRepository>().GetManyByOrganizationIdAsync(organization.Id).ReturnsNull();
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
|
.RunAsync(organization.Id, PolicyType.RequireSso)
|
||||||
|
.Returns(policy);
|
||||||
|
|
||||||
// Mock tokenable factory to return a token that expires in 5 days
|
// Mock tokenable factory to return a token that expires in 5 days
|
||||||
sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()
|
sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Models.Data;
|
using Bit.Core.AdminConsole.Models.Data;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||||
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.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
@ -13,6 +13,7 @@ using Bit.Core.Auth.Services;
|
|||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||||
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;
|
||||||
@ -163,7 +164,8 @@ public class SsoConfigServiceTests
|
|||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task SaveAsync_KeyConnector_SingleOrgNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,
|
public async Task SaveAsync_KeyConnector_SingleOrgNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,
|
||||||
Organization organization)
|
Organization organization,
|
||||||
|
[Policy(PolicyType.SingleOrg, false)] PolicyStatus policy)
|
||||||
{
|
{
|
||||||
var utcNow = DateTime.UtcNow;
|
var utcNow = DateTime.UtcNow;
|
||||||
|
|
||||||
@ -180,6 +182,9 @@ public class SsoConfigServiceTests
|
|||||||
RevisionDate = utcNow.AddDays(-10),
|
RevisionDate = utcNow.AddDays(-10),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyQuery>().RunAsync(
|
||||||
|
Arg.Any<Guid>(), PolicyType.SingleOrg).Returns(policy);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.SaveAsync(ssoConfig, organization));
|
() => sutProvider.Sut.SaveAsync(ssoConfig, organization));
|
||||||
|
|
||||||
@ -191,7 +196,9 @@ public class SsoConfigServiceTests
|
|||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task SaveAsync_KeyConnector_SsoPolicyNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,
|
public async Task SaveAsync_KeyConnector_SsoPolicyNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,
|
||||||
Organization organization)
|
Organization organization,
|
||||||
|
[Policy(PolicyType.SingleOrg, true)] PolicyStatus singleOrgPolicy,
|
||||||
|
[Policy(PolicyType.RequireSso, false)] PolicyStatus requireSsoPolicy)
|
||||||
{
|
{
|
||||||
var utcNow = DateTime.UtcNow;
|
var utcNow = DateTime.UtcNow;
|
||||||
|
|
||||||
@ -208,11 +215,10 @@ public class SsoConfigServiceTests
|
|||||||
RevisionDate = utcNow.AddDays(-10),
|
RevisionDate = utcNow.AddDays(-10),
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyRepository>().GetByOrganizationIdTypeAsync(
|
sutProvider.GetDependency<IPolicyQuery>().RunAsync(
|
||||||
Arg.Any<Guid>(), PolicyType.SingleOrg).Returns(new Policy
|
Arg.Any<Guid>(), PolicyType.SingleOrg).Returns(singleOrgPolicy);
|
||||||
{
|
sutProvider.GetDependency<IPolicyQuery>().RunAsync(
|
||||||
Enabled = true
|
Arg.Any<Guid>(), PolicyType.RequireSso).Returns(requireSsoPolicy);
|
||||||
});
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.SaveAsync(ssoConfig, organization));
|
() => sutProvider.Sut.SaveAsync(ssoConfig, organization));
|
||||||
@ -225,7 +231,8 @@ public class SsoConfigServiceTests
|
|||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task SaveAsync_KeyConnector_SsoConfigNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,
|
public async Task SaveAsync_KeyConnector_SsoConfigNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,
|
||||||
Organization organization)
|
Organization organization,
|
||||||
|
[Policy(PolicyType.SingleOrg, true)] PolicyStatus policy)
|
||||||
{
|
{
|
||||||
var utcNow = DateTime.UtcNow;
|
var utcNow = DateTime.UtcNow;
|
||||||
|
|
||||||
@ -242,11 +249,8 @@ public class SsoConfigServiceTests
|
|||||||
RevisionDate = utcNow.AddDays(-10),
|
RevisionDate = utcNow.AddDays(-10),
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyRepository>().GetByOrganizationIdTypeAsync(
|
sutProvider.GetDependency<IPolicyQuery>().RunAsync(
|
||||||
Arg.Any<Guid>(), Arg.Any<PolicyType>()).Returns(new Policy
|
Arg.Any<Guid>(), Arg.Any<PolicyType>()).Returns(policy);
|
||||||
{
|
|
||||||
Enabled = true
|
|
||||||
});
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.SaveAsync(ssoConfig, organization));
|
() => sutProvider.Sut.SaveAsync(ssoConfig, organization));
|
||||||
@ -259,7 +263,8 @@ public class SsoConfigServiceTests
|
|||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task SaveAsync_KeyConnector_KeyConnectorAbilityNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,
|
public async Task SaveAsync_KeyConnector_KeyConnectorAbilityNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,
|
||||||
Organization organization)
|
Organization organization,
|
||||||
|
[Policy(PolicyType.SingleOrg, true)] PolicyStatus policy)
|
||||||
{
|
{
|
||||||
var utcNow = DateTime.UtcNow;
|
var utcNow = DateTime.UtcNow;
|
||||||
|
|
||||||
@ -277,11 +282,8 @@ public class SsoConfigServiceTests
|
|||||||
RevisionDate = utcNow.AddDays(-10),
|
RevisionDate = utcNow.AddDays(-10),
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyRepository>().GetByOrganizationIdTypeAsync(
|
sutProvider.GetDependency<IPolicyQuery>().RunAsync(
|
||||||
Arg.Any<Guid>(), Arg.Any<PolicyType>()).Returns(new Policy
|
Arg.Any<Guid>(), Arg.Any<PolicyType>()).Returns(policy);
|
||||||
{
|
|
||||||
Enabled = true,
|
|
||||||
});
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.SaveAsync(ssoConfig, organization));
|
() => sutProvider.Sut.SaveAsync(ssoConfig, organization));
|
||||||
@ -294,7 +296,8 @@ public class SsoConfigServiceTests
|
|||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task SaveAsync_KeyConnector_Success(SutProvider<SsoConfigService> sutProvider,
|
public async Task SaveAsync_KeyConnector_Success(SutProvider<SsoConfigService> sutProvider,
|
||||||
Organization organization)
|
Organization organization,
|
||||||
|
[Policy(PolicyType.SingleOrg, true)] PolicyStatus policy)
|
||||||
{
|
{
|
||||||
var utcNow = DateTime.UtcNow;
|
var utcNow = DateTime.UtcNow;
|
||||||
|
|
||||||
@ -312,11 +315,8 @@ public class SsoConfigServiceTests
|
|||||||
RevisionDate = utcNow.AddDays(-10),
|
RevisionDate = utcNow.AddDays(-10),
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyRepository>().GetByOrganizationIdTypeAsync(
|
sutProvider.GetDependency<IPolicyQuery>().RunAsync(
|
||||||
Arg.Any<Guid>(), Arg.Any<PolicyType>()).Returns(new Policy
|
Arg.Any<Guid>(), Arg.Any<PolicyType>()).Returns(policy);
|
||||||
{
|
|
||||||
Enabled = true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await sutProvider.Sut.SaveAsync(ssoConfig, organization);
|
await sutProvider.Sut.SaveAsync(ssoConfig, organization);
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
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.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Models;
|
using Bit.Core.Auth.Models;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
@ -13,6 +14,7 @@ using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterpri
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
@ -241,7 +243,8 @@ public class RegisterUserCommandTests
|
|||||||
[BitAutoData(true, "sampleInitiationPath")]
|
[BitAutoData(true, "sampleInitiationPath")]
|
||||||
[BitAutoData(true, "Secrets Manager trial")]
|
[BitAutoData(true, "Secrets Manager trial")]
|
||||||
public async Task RegisterUserViaOrganizationInviteToken_ComplexHappyPath_Succeeds(bool addUserReferenceData, string initiationPath,
|
public async Task RegisterUserViaOrganizationInviteToken_ComplexHappyPath_Succeeds(bool addUserReferenceData, string initiationPath,
|
||||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId, Policy twoFactorPolicy)
|
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId,
|
||||||
|
[Policy(PolicyType.TwoFactorAuthentication, true)] PolicyStatus policy)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
sutProvider.GetDependency<IGlobalSettings>()
|
sutProvider.GetDependency<IGlobalSettings>()
|
||||||
@ -267,10 +270,9 @@ public class RegisterUserCommandTests
|
|||||||
.GetByIdAsync(orgUserId)
|
.GetByIdAsync(orgUserId)
|
||||||
.Returns(orgUser);
|
.Returns(orgUser);
|
||||||
|
|
||||||
twoFactorPolicy.Enabled = true;
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
sutProvider.GetDependency<IPolicyRepository>()
|
.RunAsync(orgUser.OrganizationId, PolicyType.TwoFactorAuthentication)
|
||||||
.GetByOrganizationIdTypeAsync(orgUser.OrganizationId, PolicyType.TwoFactorAuthentication)
|
.Returns(policy);
|
||||||
.Returns(twoFactorPolicy);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IUserService>()
|
sutProvider.GetDependency<IUserService>()
|
||||||
.CreateUserAsync(user, masterPasswordHash)
|
.CreateUserAsync(user, masterPasswordHash)
|
||||||
@ -286,9 +288,9 @@ public class RegisterUserCommandTests
|
|||||||
.Received(1)
|
.Received(1)
|
||||||
.GetByIdAsync(orgUserId);
|
.GetByIdAsync(orgUserId);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IPolicyRepository>()
|
await sutProvider.GetDependency<IPolicyQuery>()
|
||||||
.Received(1)
|
.Received(1)
|
||||||
.GetByOrganizationIdTypeAsync(orgUser.OrganizationId, PolicyType.TwoFactorAuthentication);
|
.RunAsync(orgUser.OrganizationId, PolicyType.TwoFactorAuthentication);
|
||||||
|
|
||||||
sutProvider.GetDependency<IUserService>()
|
sutProvider.GetDependency<IUserService>()
|
||||||
.Received(1)
|
.Received(1)
|
||||||
@ -431,7 +433,8 @@ public class RegisterUserCommandTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task RegisterUserViaOrganizationInviteToken_BlockedDomainFromDifferentOrg_ThrowsBadRequestException(
|
public async Task RegisterUserViaOrganizationInviteToken_BlockedDomainFromDifferentOrg_ThrowsBadRequestException(
|
||||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId)
|
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId,
|
||||||
|
[Policy(PolicyType.TwoFactorAuthentication, false)] PolicyStatus policy)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
user.Email = "user@blocked-domain.com";
|
user.Email = "user@blocked-domain.com";
|
||||||
@ -463,6 +466,10 @@ public class RegisterUserCommandTests
|
|||||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com", orgUser.OrganizationId)
|
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com", orgUser.OrganizationId)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
|
.RunAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)
|
||||||
|
.Returns(policy);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId));
|
sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId));
|
||||||
@ -472,7 +479,8 @@ public class RegisterUserCommandTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task RegisterUserViaOrganizationInviteToken_BlockedDomainFromSameOrg_Succeeds(
|
public async Task RegisterUserViaOrganizationInviteToken_BlockedDomainFromSameOrg_Succeeds(
|
||||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId)
|
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId,
|
||||||
|
[Policy(PolicyType.TwoFactorAuthentication, false)] PolicyStatus policy)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
user.Email = "user@company-domain.com";
|
user.Email = "user@company-domain.com";
|
||||||
@ -509,6 +517,10 @@ public class RegisterUserCommandTests
|
|||||||
.CreateUserAsync(user, masterPasswordHash)
|
.CreateUserAsync(user, masterPasswordHash)
|
||||||
.Returns(IdentityResult.Success);
|
.Returns(IdentityResult.Success);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
|
.RunAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)
|
||||||
|
.Returns(policy);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId);
|
var result = await sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId);
|
||||||
|
|
||||||
@ -1245,6 +1257,7 @@ public class RegisterUserCommandTests
|
|||||||
OrganizationUser orgUser,
|
OrganizationUser orgUser,
|
||||||
string orgInviteToken,
|
string orgInviteToken,
|
||||||
string masterPasswordHash,
|
string masterPasswordHash,
|
||||||
|
[Policy(PolicyType.TwoFactorAuthentication, false)] PolicyStatus policy,
|
||||||
SutProvider<RegisterUserCommand> sutProvider)
|
SutProvider<RegisterUserCommand> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@ -1259,9 +1272,9 @@ public class RegisterUserCommandTests
|
|||||||
.GetByIdAsync(orgUser.Id)
|
.GetByIdAsync(orgUser.Id)
|
||||||
.Returns(orgUser);
|
.Returns(orgUser);
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyRepository>()
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
.GetByOrganizationIdTypeAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)
|
.RunAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)
|
||||||
.Returns((Policy)null);
|
.Returns(policy);
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>()
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
.GetByIdAsync(orgUser.OrganizationId)
|
.GetByIdAsync(orgUser.OrganizationId)
|
||||||
@ -1331,6 +1344,7 @@ public class RegisterUserCommandTests
|
|||||||
OrganizationUser orgUser,
|
OrganizationUser orgUser,
|
||||||
string masterPasswordHash,
|
string masterPasswordHash,
|
||||||
string orgInviteToken,
|
string orgInviteToken,
|
||||||
|
[Policy(PolicyType.TwoFactorAuthentication, false)] PolicyStatus policy,
|
||||||
SutProvider<RegisterUserCommand> sutProvider)
|
SutProvider<RegisterUserCommand> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@ -1346,9 +1360,9 @@ public class RegisterUserCommandTests
|
|||||||
.GetByIdAsync(orgUser.Id)
|
.GetByIdAsync(orgUser.Id)
|
||||||
.Returns(orgUser);
|
.Returns(orgUser);
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyRepository>()
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
.GetByOrganizationIdTypeAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)
|
.RunAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)
|
||||||
.Returns((Policy)null);
|
.Returns(policy);
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>()
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
.GetByIdAsync(orgUser.OrganizationId)
|
.GetByIdAsync(orgUser.OrganizationId)
|
||||||
|
|||||||
@ -0,0 +1,777 @@
|
|||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Payment.Models;
|
||||||
|
using Bit.Core.Billing.Premium.Commands;
|
||||||
|
using Bit.Core.Billing.Pricing;
|
||||||
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Test.Billing.Mocks.Plans;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NSubstitute;
|
||||||
|
using Stripe;
|
||||||
|
using Xunit;
|
||||||
|
using PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Billing.Premium.Commands;
|
||||||
|
|
||||||
|
public class PreviewPremiumUpgradeProrationCommandTests
|
||||||
|
{
|
||||||
|
private readonly ILogger<PreviewPremiumUpgradeProrationCommand> _logger = Substitute.For<ILogger<PreviewPremiumUpgradeProrationCommand>>();
|
||||||
|
private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();
|
||||||
|
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
|
||||||
|
private readonly PreviewPremiumUpgradeProrationCommand _command;
|
||||||
|
|
||||||
|
public PreviewPremiumUpgradeProrationCommandTests()
|
||||||
|
{
|
||||||
|
_command = new PreviewPremiumUpgradeProrationCommand(
|
||||||
|
_logger,
|
||||||
|
_pricingClient,
|
||||||
|
_stripeAdapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_UserWithoutPremium_ReturnsBadRequest(User user, BillingAddress billingAddress)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Premium = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _command.Run(user, PlanType.TeamsAnnually, billingAddress);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsT1);
|
||||||
|
var badRequest = result.AsT1;
|
||||||
|
Assert.Equal("User does not have an active Premium subscription.", badRequest.Response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_UserWithoutGatewaySubscriptionId_ReturnsBadRequest(User user, BillingAddress billingAddress)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Premium = true;
|
||||||
|
user.GatewaySubscriptionId = null;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _command.Run(user, PlanType.TeamsAnnually, billingAddress);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsT1);
|
||||||
|
var badRequest = result.AsT1;
|
||||||
|
Assert.Equal("User does not have an active Premium subscription.", badRequest.Response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_ValidUpgrade_ReturnsProrationAmounts(User user, BillingAddress billingAddress)
|
||||||
|
{
|
||||||
|
// Arrange - Setup valid Premium user
|
||||||
|
user.Premium = true;
|
||||||
|
user.GatewaySubscriptionId = "sub_123";
|
||||||
|
user.GatewayCustomerId = "cus_123";
|
||||||
|
|
||||||
|
// Setup Premium plans
|
||||||
|
var premiumPlan = new PremiumPlan
|
||||||
|
{
|
||||||
|
Name = "Premium",
|
||||||
|
Available = true,
|
||||||
|
LegacyYear = null,
|
||||||
|
Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "premium-annually",
|
||||||
|
Price = 10m,
|
||||||
|
Provided = 1
|
||||||
|
},
|
||||||
|
Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "storage-gb-annually",
|
||||||
|
Price = 4m,
|
||||||
|
Provided = 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var premiumPlans = new List<PremiumPlan> { premiumPlan };
|
||||||
|
|
||||||
|
// Setup current Stripe subscription
|
||||||
|
var now = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
var currentPeriodEnd = now.AddMonths(6);
|
||||||
|
var currentSubscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = "sub_123",
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
Id = "cus_123",
|
||||||
|
Discount = null
|
||||||
|
},
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = "si_premium",
|
||||||
|
Price = new Price { Id = "premium-annually" },
|
||||||
|
CurrentPeriodEnd = currentPeriodEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup target organization plan
|
||||||
|
var targetPlan = new TeamsPlan(isAnnual: true);
|
||||||
|
|
||||||
|
// Setup invoice preview response
|
||||||
|
var invoice = new Invoice
|
||||||
|
{
|
||||||
|
Total = 5000, // $50.00
|
||||||
|
TotalTaxes = new List<InvoiceTotalTax>
|
||||||
|
{
|
||||||
|
new() { Amount = 500 } // $5.00
|
||||||
|
},
|
||||||
|
Lines = new StripeList<InvoiceLineItem>
|
||||||
|
{
|
||||||
|
Data = new List<InvoiceLineItem>
|
||||||
|
{
|
||||||
|
new() { Amount = 5000 } // $50.00 for new plan
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PeriodEnd = now
|
||||||
|
};
|
||||||
|
|
||||||
|
// Configure mocks
|
||||||
|
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(targetPlan);
|
||||||
|
_stripeAdapter.GetSubscriptionAsync(
|
||||||
|
"sub_123",
|
||||||
|
Arg.Any<SubscriptionGetOptions>())
|
||||||
|
.Returns(currentSubscription);
|
||||||
|
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||||
|
.Returns(invoice);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _command.Run(user, PlanType.TeamsAnnually, billingAddress);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsT0);
|
||||||
|
var proration = result.AsT0;
|
||||||
|
Assert.Equal(50.00m, proration.NewPlanProratedAmount);
|
||||||
|
Assert.Equal(0m, proration.Credit);
|
||||||
|
Assert.Equal(5.00m, proration.Tax);
|
||||||
|
Assert.Equal(50.00m, proration.Total);
|
||||||
|
Assert.Equal(6, proration.NewPlanProratedMonths); // 6 months remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_ValidUpgrade_ExtractsProrationCredit(User user, BillingAddress billingAddress)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Premium = true;
|
||||||
|
user.GatewaySubscriptionId = "sub_123";
|
||||||
|
user.GatewayCustomerId = "cus_123";
|
||||||
|
|
||||||
|
var premiumPlan = new PremiumPlan
|
||||||
|
{
|
||||||
|
Name = "Premium",
|
||||||
|
Available = true,
|
||||||
|
LegacyYear = null,
|
||||||
|
Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "premium-annually",
|
||||||
|
Price = 10m,
|
||||||
|
Provided = 1
|
||||||
|
},
|
||||||
|
Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "storage-gb-annually",
|
||||||
|
Price = 4m,
|
||||||
|
Provided = 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var premiumPlans = new List<PremiumPlan> { premiumPlan };
|
||||||
|
|
||||||
|
// Use fixed time to avoid DateTime.UtcNow differences
|
||||||
|
var now = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
var currentPeriodEnd = now.AddDays(45); // 1.5 months ~ 2 months rounded
|
||||||
|
var currentSubscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = "sub_123",
|
||||||
|
Customer = new Customer { Id = "cus_123", Discount = null },
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new() { Id = "si_premium", Price = new Price { Id = "premium-annually" }, CurrentPeriodEnd = currentPeriodEnd }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var targetPlan = new TeamsPlan(isAnnual: true);
|
||||||
|
|
||||||
|
// Invoice with negative line item (proration credit)
|
||||||
|
var invoice = new Invoice
|
||||||
|
{
|
||||||
|
Total = 4000, // $40.00
|
||||||
|
TotalTaxes = new List<InvoiceTotalTax> { new() { Amount = 400 } }, // $4.00
|
||||||
|
Lines = new StripeList<InvoiceLineItem>
|
||||||
|
{
|
||||||
|
Data = new List<InvoiceLineItem>
|
||||||
|
{
|
||||||
|
new() { Amount = -1000 }, // -$10.00 credit from unused Premium
|
||||||
|
new() { Amount = 5000 } // $50.00 for new plan
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PeriodEnd = now
|
||||||
|
};
|
||||||
|
|
||||||
|
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(targetPlan);
|
||||||
|
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>())
|
||||||
|
.Returns(currentSubscription);
|
||||||
|
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||||
|
.Returns(invoice);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _command.Run(user, PlanType.TeamsAnnually, billingAddress);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsT0);
|
||||||
|
var proration = result.AsT0;
|
||||||
|
Assert.Equal(50.00m, proration.NewPlanProratedAmount);
|
||||||
|
Assert.Equal(10.00m, proration.Credit); // Proration credit
|
||||||
|
Assert.Equal(4.00m, proration.Tax);
|
||||||
|
Assert.Equal(40.00m, proration.Total);
|
||||||
|
Assert.Equal(2, proration.NewPlanProratedMonths); // 45 days rounds to 2 months
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_ValidUpgrade_AlwaysUsesOneSeat(User user, BillingAddress billingAddress)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Premium = true;
|
||||||
|
user.GatewaySubscriptionId = "sub_123";
|
||||||
|
user.GatewayCustomerId = "cus_123";
|
||||||
|
|
||||||
|
var premiumPlan = new PremiumPlan
|
||||||
|
{
|
||||||
|
Name = "Premium",
|
||||||
|
Available = true,
|
||||||
|
LegacyYear = null,
|
||||||
|
Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "premium-annually",
|
||||||
|
Price = 10m,
|
||||||
|
Provided = 1
|
||||||
|
},
|
||||||
|
Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "storage-gb-annually",
|
||||||
|
Price = 4m,
|
||||||
|
Provided = 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var premiumPlans = new List<PremiumPlan> { premiumPlan };
|
||||||
|
|
||||||
|
var currentSubscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = "sub_123",
|
||||||
|
Customer = new Customer { Id = "cus_123", Discount = null },
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new() { Id = "si_premium", Price = new Price { Id = "premium-annually" }, CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var targetPlan = new TeamsPlan(isAnnual: true);
|
||||||
|
|
||||||
|
var invoice = new Invoice
|
||||||
|
{
|
||||||
|
Total = 5000,
|
||||||
|
TotalTaxes = new List<InvoiceTotalTax> { new() { Amount = 500 } },
|
||||||
|
Lines = new StripeList<InvoiceLineItem> { Data = new List<InvoiceLineItem> { new() { Amount = 5000 } } },
|
||||||
|
PeriodEnd = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(targetPlan);
|
||||||
|
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>())
|
||||||
|
.Returns(currentSubscription);
|
||||||
|
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||||
|
.Returns(invoice);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _command.Run(user, PlanType.TeamsAnnually, billingAddress);
|
||||||
|
|
||||||
|
// Assert - Verify that the subscription item quantity is always 1 and has Id
|
||||||
|
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(
|
||||||
|
Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||||
|
options.SubscriptionDetails.Items.Any(item =>
|
||||||
|
item.Id == "si_premium" &&
|
||||||
|
item.Price == targetPlan.PasswordManager.StripeSeatPlanId &&
|
||||||
|
item.Quantity == 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_ValidUpgrade_DeletesPremiumSubscriptionItems(User user, BillingAddress billingAddress)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Premium = true;
|
||||||
|
user.GatewaySubscriptionId = "sub_123";
|
||||||
|
user.GatewayCustomerId = "cus_123";
|
||||||
|
|
||||||
|
var premiumPlan = new PremiumPlan
|
||||||
|
{
|
||||||
|
Name = "Premium",
|
||||||
|
Available = true,
|
||||||
|
LegacyYear = null,
|
||||||
|
Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "premium-annually",
|
||||||
|
Price = 10m,
|
||||||
|
Provided = 1
|
||||||
|
},
|
||||||
|
Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "storage-gb-annually",
|
||||||
|
Price = 4m,
|
||||||
|
Provided = 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var premiumPlans = new List<PremiumPlan> { premiumPlan };
|
||||||
|
|
||||||
|
var currentSubscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = "sub_123",
|
||||||
|
Customer = new Customer { Id = "cus_123", Discount = null },
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new() { Id = "si_password_manager", Price = new Price { Id = "premium-annually" }, CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) },
|
||||||
|
new() { Id = "si_storage", Price = new Price { Id = "storage-gb-annually" }, CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var targetPlan = new TeamsPlan(isAnnual: true);
|
||||||
|
|
||||||
|
var invoice = new Invoice
|
||||||
|
{
|
||||||
|
Total = 5000,
|
||||||
|
TotalTaxes = new List<InvoiceTotalTax> { new() { Amount = 500 } },
|
||||||
|
Lines = new StripeList<InvoiceLineItem> { Data = new List<InvoiceLineItem> { new() { Amount = 5000 } } },
|
||||||
|
PeriodEnd = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(targetPlan);
|
||||||
|
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>())
|
||||||
|
.Returns(currentSubscription);
|
||||||
|
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||||
|
.Returns(invoice);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _command.Run(user, PlanType.TeamsAnnually, billingAddress);
|
||||||
|
|
||||||
|
// Assert - Verify password manager item is modified and storage item is deleted
|
||||||
|
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(
|
||||||
|
Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||||
|
// Password manager item should be modified to new plan price, not deleted
|
||||||
|
options.SubscriptionDetails.Items.Any(item =>
|
||||||
|
item.Id == "si_password_manager" &&
|
||||||
|
item.Price == targetPlan.PasswordManager.StripeSeatPlanId &&
|
||||||
|
item.Deleted != true) &&
|
||||||
|
// Storage item should be deleted
|
||||||
|
options.SubscriptionDetails.Items.Any(item =>
|
||||||
|
item.Id == "si_storage" && item.Deleted == true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_NonSeatBasedPlan_UsesStripePlanId(User user, BillingAddress billingAddress)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Premium = true;
|
||||||
|
user.GatewaySubscriptionId = "sub_123";
|
||||||
|
user.GatewayCustomerId = "cus_123";
|
||||||
|
|
||||||
|
var premiumPlan = new PremiumPlan
|
||||||
|
{
|
||||||
|
Name = "Premium",
|
||||||
|
Available = true,
|
||||||
|
LegacyYear = null,
|
||||||
|
Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "premium-annually",
|
||||||
|
Price = 10m,
|
||||||
|
Provided = 1
|
||||||
|
},
|
||||||
|
Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "storage-gb-annually",
|
||||||
|
Price = 4m,
|
||||||
|
Provided = 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var premiumPlans = new List<PremiumPlan> { premiumPlan };
|
||||||
|
|
||||||
|
var currentSubscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = "sub_123",
|
||||||
|
Customer = new Customer { Id = "cus_123", Discount = null },
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new() { Id = "si_premium", Price = new Price { Id = "premium-annually" }, CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var targetPlan = new FamiliesPlan(); // families is non seat based
|
||||||
|
|
||||||
|
var invoice = new Invoice
|
||||||
|
{
|
||||||
|
Total = 5000,
|
||||||
|
TotalTaxes = new List<InvoiceTotalTax> { new() { Amount = 500 } },
|
||||||
|
Lines = new StripeList<InvoiceLineItem> { Data = new List<InvoiceLineItem> { new() { Amount = 5000 } } },
|
||||||
|
PeriodEnd = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(targetPlan);
|
||||||
|
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>())
|
||||||
|
.Returns(currentSubscription);
|
||||||
|
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||||
|
.Returns(invoice);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _command.Run(user, PlanType.FamiliesAnnually, billingAddress);
|
||||||
|
|
||||||
|
// Assert - Verify non-seat-based plan uses StripePlanId with quantity 1 and modifies existing item
|
||||||
|
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(
|
||||||
|
Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||||
|
options.SubscriptionDetails.Items.Any(item =>
|
||||||
|
item.Id == "si_premium" &&
|
||||||
|
item.Price == targetPlan.PasswordManager.StripePlanId &&
|
||||||
|
item.Quantity == 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_ValidUpgrade_CreatesCorrectInvoicePreviewOptions(User user, BillingAddress billingAddress)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Premium = true;
|
||||||
|
user.GatewaySubscriptionId = "sub_123";
|
||||||
|
user.GatewayCustomerId = "cus_123";
|
||||||
|
billingAddress.Country = "US";
|
||||||
|
billingAddress.PostalCode = "12345";
|
||||||
|
|
||||||
|
var premiumPlan = new PremiumPlan
|
||||||
|
{
|
||||||
|
Name = "Premium",
|
||||||
|
Available = true,
|
||||||
|
LegacyYear = null,
|
||||||
|
Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "premium-annually",
|
||||||
|
Price = 10m,
|
||||||
|
Provided = 1
|
||||||
|
},
|
||||||
|
Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "storage-gb-annually",
|
||||||
|
Price = 4m,
|
||||||
|
Provided = 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var premiumPlans = new List<PremiumPlan> { premiumPlan };
|
||||||
|
|
||||||
|
var currentSubscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = "sub_123",
|
||||||
|
Customer = new Customer { Id = "cus_123", Discount = null },
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new() { Id = "si_premium", Price = new Price { Id = "premium-annually" }, CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var targetPlan = new TeamsPlan(isAnnual: true);
|
||||||
|
|
||||||
|
var invoice = new Invoice
|
||||||
|
{
|
||||||
|
Total = 5000,
|
||||||
|
TotalTaxes = new List<InvoiceTotalTax> { new() { Amount = 500 } },
|
||||||
|
Lines = new StripeList<InvoiceLineItem> { Data = new List<InvoiceLineItem> { new() { Amount = 5000 } } },
|
||||||
|
PeriodEnd = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(targetPlan);
|
||||||
|
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>())
|
||||||
|
.Returns(currentSubscription);
|
||||||
|
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||||
|
.Returns(invoice);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _command.Run(user, PlanType.TeamsAnnually, billingAddress);
|
||||||
|
|
||||||
|
// Assert - Verify all invoice preview options are correct
|
||||||
|
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(
|
||||||
|
Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||||
|
options.AutomaticTax.Enabled == true &&
|
||||||
|
options.Customer == "cus_123" &&
|
||||||
|
options.Subscription == "sub_123" &&
|
||||||
|
options.CustomerDetails.Address.Country == "US" &&
|
||||||
|
options.CustomerDetails.Address.PostalCode == "12345" &&
|
||||||
|
options.SubscriptionDetails.ProrationBehavior == "always_invoice"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_SeatBasedPlan_UsesStripeSeatPlanId(User user, BillingAddress billingAddress)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Premium = true;
|
||||||
|
user.GatewaySubscriptionId = "sub_123";
|
||||||
|
user.GatewayCustomerId = "cus_123";
|
||||||
|
|
||||||
|
var premiumPlan = new PremiumPlan
|
||||||
|
{
|
||||||
|
Name = "Premium",
|
||||||
|
Available = true,
|
||||||
|
LegacyYear = null,
|
||||||
|
Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "premium-annually",
|
||||||
|
Price = 10m,
|
||||||
|
Provided = 1
|
||||||
|
},
|
||||||
|
Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "storage-gb-annually",
|
||||||
|
Price = 4m,
|
||||||
|
Provided = 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var premiumPlans = new List<PremiumPlan> { premiumPlan };
|
||||||
|
|
||||||
|
var currentSubscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = "sub_123",
|
||||||
|
Customer = new Customer { Id = "cus_123", Discount = null },
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new() { Id = "si_premium", Price = new Price { Id = "premium-annually" }, CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use Teams which is seat-based
|
||||||
|
var targetPlan = new TeamsPlan(isAnnual: true);
|
||||||
|
|
||||||
|
var invoice = new Invoice
|
||||||
|
{
|
||||||
|
Total = 5000,
|
||||||
|
TotalTaxes = new List<InvoiceTotalTax> { new() { Amount = 500 } },
|
||||||
|
Lines = new StripeList<InvoiceLineItem> { Data = new List<InvoiceLineItem> { new() { Amount = 5000 } } },
|
||||||
|
PeriodEnd = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(targetPlan);
|
||||||
|
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>())
|
||||||
|
.Returns(currentSubscription);
|
||||||
|
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||||
|
.Returns(invoice);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _command.Run(user, PlanType.TeamsAnnually, billingAddress);
|
||||||
|
|
||||||
|
// Assert - Verify seat-based plan uses StripeSeatPlanId with quantity 1 and modifies existing item
|
||||||
|
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(
|
||||||
|
Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||||
|
options.SubscriptionDetails.Items.Any(item =>
|
||||||
|
item.Id == "si_premium" &&
|
||||||
|
item.Price == targetPlan.PasswordManager.StripeSeatPlanId &&
|
||||||
|
item.Quantity == 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0, 1)] // Less than 15 days, minimum 1 month
|
||||||
|
[InlineData(1, 1)] // 1 day = 1 month minimum
|
||||||
|
[InlineData(14, 1)] // 14 days = 1 month minimum
|
||||||
|
[InlineData(15, 1)] // 15 days rounds to 1 month
|
||||||
|
[InlineData(30, 1)] // 30 days = 1 month
|
||||||
|
[InlineData(44, 1)] // 44 days rounds to 1 month
|
||||||
|
[InlineData(45, 2)] // 45 days rounds to 2 months
|
||||||
|
[InlineData(60, 2)] // 60 days = 2 months
|
||||||
|
[InlineData(90, 3)] // 90 days = 3 months
|
||||||
|
[InlineData(180, 6)] // 180 days = 6 months
|
||||||
|
[InlineData(365, 12)] // 365 days rounds to 12 months
|
||||||
|
public async Task Run_ValidUpgrade_CalculatesNewPlanProratedMonthsCorrectly(int daysRemaining, int expectedMonths)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Premium = true,
|
||||||
|
GatewaySubscriptionId = "sub_123",
|
||||||
|
GatewayCustomerId = "cus_123"
|
||||||
|
};
|
||||||
|
var billingAddress = new Core.Billing.Payment.Models.BillingAddress
|
||||||
|
{
|
||||||
|
Country = "US",
|
||||||
|
PostalCode = "12345"
|
||||||
|
};
|
||||||
|
|
||||||
|
var premiumPlan = new PremiumPlan
|
||||||
|
{
|
||||||
|
Name = "Premium",
|
||||||
|
Available = true,
|
||||||
|
LegacyYear = null,
|
||||||
|
Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "premium-annually",
|
||||||
|
Price = 10m,
|
||||||
|
Provided = 1
|
||||||
|
},
|
||||||
|
Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "storage-gb-annually",
|
||||||
|
Price = 4m,
|
||||||
|
Provided = 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var premiumPlans = new List<PremiumPlan> { premiumPlan };
|
||||||
|
|
||||||
|
// Use fixed time to avoid DateTime.UtcNow differences
|
||||||
|
var now = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
var currentPeriodEnd = now.AddDays(daysRemaining);
|
||||||
|
var currentSubscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = "sub_123",
|
||||||
|
Customer = new Customer { Id = "cus_123", Discount = null },
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new() { Id = "si_premium", Price = new Price { Id = "premium-annually" }, CurrentPeriodEnd = currentPeriodEnd }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var targetPlan = new TeamsPlan(isAnnual: true);
|
||||||
|
|
||||||
|
var invoice = new Invoice
|
||||||
|
{
|
||||||
|
Total = 5000,
|
||||||
|
TotalTaxes = new List<InvoiceTotalTax> { new() { Amount = 500 } },
|
||||||
|
Lines = new StripeList<InvoiceLineItem>
|
||||||
|
{
|
||||||
|
Data = new List<InvoiceLineItem> { new() { Amount = 5000 } }
|
||||||
|
},
|
||||||
|
PeriodEnd = now
|
||||||
|
};
|
||||||
|
|
||||||
|
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(targetPlan);
|
||||||
|
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>())
|
||||||
|
.Returns(currentSubscription);
|
||||||
|
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||||
|
.Returns(invoice);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _command.Run(user, PlanType.TeamsAnnually, billingAddress);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsT0);
|
||||||
|
var proration = result.AsT0;
|
||||||
|
Assert.Equal(expectedMonths, proration.NewPlanProratedMonths);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_ValidUpgrade_ReturnsNewPlanProratedAmountCorrectly(User user, BillingAddress billingAddress)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Premium = true;
|
||||||
|
user.GatewaySubscriptionId = "sub_123";
|
||||||
|
user.GatewayCustomerId = "cus_123";
|
||||||
|
|
||||||
|
var premiumPlan = new PremiumPlan
|
||||||
|
{
|
||||||
|
Name = "Premium",
|
||||||
|
Available = true,
|
||||||
|
LegacyYear = null,
|
||||||
|
Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "premium-annually",
|
||||||
|
Price = 10m,
|
||||||
|
Provided = 1
|
||||||
|
},
|
||||||
|
Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable
|
||||||
|
{
|
||||||
|
StripePriceId = "storage-gb-annually",
|
||||||
|
Price = 4m,
|
||||||
|
Provided = 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var premiumPlans = new List<PremiumPlan> { premiumPlan };
|
||||||
|
|
||||||
|
var now = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
var currentPeriodEnd = now.AddMonths(3);
|
||||||
|
var currentSubscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = "sub_123",
|
||||||
|
Customer = new Customer { Id = "cus_123", Discount = null },
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new() { Id = "si_premium", Price = new Price { Id = "premium-annually" }, CurrentPeriodEnd = currentPeriodEnd }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var targetPlan = new TeamsPlan(isAnnual: true);
|
||||||
|
|
||||||
|
// Invoice showing new plan cost, credit, and net
|
||||||
|
var invoice = new Invoice
|
||||||
|
{
|
||||||
|
Total = 4500, // $45.00 net after $5 credit
|
||||||
|
TotalTaxes = new List<InvoiceTotalTax> { new() { Amount = 450 } }, // $4.50
|
||||||
|
Lines = new StripeList<InvoiceLineItem>
|
||||||
|
{
|
||||||
|
Data = new List<InvoiceLineItem>
|
||||||
|
{
|
||||||
|
new() { Amount = -500 }, // -$5.00 credit
|
||||||
|
new() { Amount = 5000 } // $50.00 for new plan
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PeriodEnd = now
|
||||||
|
};
|
||||||
|
|
||||||
|
_pricingClient.ListPremiumPlans().Returns(premiumPlans);
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(targetPlan);
|
||||||
|
_stripeAdapter.GetSubscriptionAsync("sub_123", Arg.Any<SubscriptionGetOptions>())
|
||||||
|
.Returns(currentSubscription);
|
||||||
|
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||||
|
.Returns(invoice);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _command.Run(user, PlanType.TeamsAnnually, billingAddress);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsT0);
|
||||||
|
var proration = result.AsT0;
|
||||||
|
|
||||||
|
Assert.Equal(50.00m, proration.NewPlanProratedAmount);
|
||||||
|
Assert.Equal(5.00m, proration.Credit);
|
||||||
|
Assert.Equal(4.50m, proration.Tax);
|
||||||
|
Assert.Equal(45.00m, proration.Total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -37,7 +37,6 @@ public class UpgradePremiumToOrganizationCommandTests
|
|||||||
NameLocalizationKey = "";
|
NameLocalizationKey = "";
|
||||||
DescriptionLocalizationKey = "";
|
DescriptionLocalizationKey = "";
|
||||||
CanBeUsedByBusiness = true;
|
CanBeUsedByBusiness = true;
|
||||||
TrialPeriodDays = null;
|
|
||||||
HasSelfHost = false;
|
HasSelfHost = false;
|
||||||
HasPolicies = false;
|
HasPolicies = false;
|
||||||
HasGroups = false;
|
HasGroups = false;
|
||||||
@ -86,10 +85,8 @@ public class UpgradePremiumToOrganizationCommandTests
|
|||||||
string? stripePlanId = null,
|
string? stripePlanId = null,
|
||||||
string? stripeSeatPlanId = null,
|
string? stripeSeatPlanId = null,
|
||||||
string? stripePremiumAccessPlanId = null,
|
string? stripePremiumAccessPlanId = null,
|
||||||
string? stripeStoragePlanId = null)
|
string? stripeStoragePlanId = null) =>
|
||||||
{
|
new TestPlan(planType, stripePlanId, stripeSeatPlanId, stripePremiumAccessPlanId, stripeStoragePlanId);
|
||||||
return new TestPlan(planType, stripePlanId, stripeSeatPlanId, stripePremiumAccessPlanId, stripeStoragePlanId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PremiumPlan CreateTestPremiumPlan(
|
private static PremiumPlan CreateTestPremiumPlan(
|
||||||
string seatPriceId = "premium-annually",
|
string seatPriceId = "premium-annually",
|
||||||
@ -151,6 +148,9 @@ public class UpgradePremiumToOrganizationCommandTests
|
|||||||
_applicationCacheService);
|
_applicationCacheService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Core.Billing.Payment.Models.BillingAddress CreateTestBillingAddress() =>
|
||||||
|
new() { Country = "US", PostalCode = "12345" };
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Run_UserNotPremium_ReturnsBadRequest(User user)
|
public async Task Run_UserNotPremium_ReturnsBadRequest(User user)
|
||||||
{
|
{
|
||||||
@ -158,7 +158,7 @@ public class UpgradePremiumToOrganizationCommandTests
|
|||||||
user.Premium = false;
|
user.Premium = false;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually);
|
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(result.IsT1);
|
Assert.True(result.IsT1);
|
||||||
@ -174,7 +174,7 @@ public class UpgradePremiumToOrganizationCommandTests
|
|||||||
user.GatewaySubscriptionId = null;
|
user.GatewaySubscriptionId = null;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually);
|
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(result.IsT1);
|
Assert.True(result.IsT1);
|
||||||
@ -190,7 +190,7 @@ public class UpgradePremiumToOrganizationCommandTests
|
|||||||
user.GatewaySubscriptionId = "";
|
user.GatewaySubscriptionId = "";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually);
|
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(result.IsT1);
|
Assert.True(result.IsT1);
|
||||||
@ -245,7 +245,7 @@ public class UpgradePremiumToOrganizationCommandTests
|
|||||||
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually);
|
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(result.IsT0);
|
Assert.True(result.IsT0);
|
||||||
@ -253,9 +253,8 @@ public class UpgradePremiumToOrganizationCommandTests
|
|||||||
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
||||||
"sub_123",
|
"sub_123",
|
||||||
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
||||||
opts.Items.Count == 2 && // 1 deleted + 1 seat (no storage)
|
opts.Items.Count == 1 && // Only 1 item: modify existing password manager item (no storage to delete)
|
||||||
opts.Items.Any(i => i.Deleted == true) &&
|
opts.Items.Any(i => i.Id == "si_premium" && i.Price == "teams-seat-annually" && i.Quantity == 1 && i.Deleted != true)));
|
||||||
opts.Items.Any(i => i.Price == "teams-seat-annually" && i.Quantity == 1)));
|
|
||||||
|
|
||||||
await _organizationRepository.Received(1).CreateAsync(Arg.Is<Organization>(o =>
|
await _organizationRepository.Received(1).CreateAsync(Arg.Is<Organization>(o =>
|
||||||
o.Name == "My Organization" &&
|
o.Name == "My Organization" &&
|
||||||
@ -320,7 +319,7 @@ public class UpgradePremiumToOrganizationCommandTests
|
|||||||
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await _command.Run(user, "My Families Org", "encrypted-key", PlanType.FamiliesAnnually);
|
var result = await _command.Run(user, "My Families Org", "encrypted-key", PlanType.FamiliesAnnually, CreateTestBillingAddress());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(result.IsT0);
|
Assert.True(result.IsT0);
|
||||||
@ -328,9 +327,8 @@ public class UpgradePremiumToOrganizationCommandTests
|
|||||||
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
||||||
"sub_123",
|
"sub_123",
|
||||||
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
||||||
opts.Items.Count == 2 && // 1 deleted + 1 plan
|
opts.Items.Count == 1 && // Only 1 item: modify existing password manager item (no storage to delete)
|
||||||
opts.Items.Any(i => i.Deleted == true) &&
|
opts.Items.Any(i => i.Id == "si_premium" && i.Price == "families-plan-annually" && i.Quantity == 1 && i.Deleted != true)));
|
||||||
opts.Items.Any(i => i.Price == "families-plan-annually" && i.Quantity == 1)));
|
|
||||||
|
|
||||||
await _organizationRepository.Received(1).CreateAsync(Arg.Is<Organization>(o =>
|
await _organizationRepository.Received(1).CreateAsync(Arg.Is<Organization>(o =>
|
||||||
o.Name == "My Families Org"));
|
o.Name == "My Families Org"));
|
||||||
@ -383,7 +381,7 @@ public class UpgradePremiumToOrganizationCommandTests
|
|||||||
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually);
|
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(result.IsT0);
|
Assert.True(result.IsT0);
|
||||||
@ -392,11 +390,6 @@ public class UpgradePremiumToOrganizationCommandTests
|
|||||||
"sub_123",
|
"sub_123",
|
||||||
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
||||||
opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId) &&
|
opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId) &&
|
||||||
opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.PreviousPremiumPriceId) &&
|
|
||||||
opts.Metadata[StripeConstants.MetadataKeys.PreviousPremiumPriceId] == "premium-annually" &&
|
|
||||||
opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.PreviousPeriodEndDate) &&
|
|
||||||
opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.PreviousAdditionalStorage) &&
|
|
||||||
opts.Metadata[StripeConstants.MetadataKeys.PreviousAdditionalStorage] == "0" &&
|
|
||||||
opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.UserId) &&
|
opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.UserId) &&
|
||||||
opts.Metadata[StripeConstants.MetadataKeys.UserId] == string.Empty)); // Removes userId to unlink from User
|
opts.Metadata[StripeConstants.MetadataKeys.UserId] == string.Empty)); // Removes userId to unlink from User
|
||||||
}
|
}
|
||||||
@ -453,19 +446,18 @@ public class UpgradePremiumToOrganizationCommandTests
|
|||||||
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually);
|
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(result.IsT0);
|
Assert.True(result.IsT0);
|
||||||
|
|
||||||
// Verify that BOTH legacy items (password manager + storage) are deleted by ID
|
// Verify that legacy password manager item is modified and legacy storage is deleted
|
||||||
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
||||||
"sub_123",
|
"sub_123",
|
||||||
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
||||||
opts.Items.Count == 3 && // 2 deleted (legacy PM + legacy storage) + 1 new seat
|
opts.Items.Count == 2 && // 1 modified (legacy PM to new price) + 1 deleted (legacy storage)
|
||||||
opts.Items.Count(i => i.Deleted == true && i.Id == "si_premium_legacy") == 1 && // Legacy PM deleted
|
opts.Items.Count(i => i.Id == "si_premium_legacy" && i.Price == "teams-seat-annually" && i.Quantity == 1 && i.Deleted != true) == 1 && // Legacy PM modified
|
||||||
opts.Items.Count(i => i.Deleted == true && i.Id == "si_storage_legacy") == 1 && // Legacy storage deleted
|
opts.Items.Count(i => i.Deleted == true && i.Id == "si_storage_legacy") == 1)); // Legacy storage deleted
|
||||||
opts.Items.Any(i => i.Price == "teams-seat-annually" && i.Quantity == 1)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
@ -520,20 +512,19 @@ public class UpgradePremiumToOrganizationCommandTests
|
|||||||
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually);
|
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(result.IsT0);
|
Assert.True(result.IsT0);
|
||||||
|
|
||||||
// Verify that ONLY the premium password manager item is deleted (not other products)
|
// Verify that ONLY the premium password manager item is modified (not other products)
|
||||||
// Note: We delete the specific premium item by ID, so other products are untouched
|
// Note: We modify the specific premium item by ID, so other products are untouched
|
||||||
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
||||||
"sub_123",
|
"sub_123",
|
||||||
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
||||||
opts.Items.Count == 2 && // 1 deleted (premium password manager) + 1 new seat
|
opts.Items.Count == 1 && // Only modify premium password manager item
|
||||||
opts.Items.Count(i => i.Deleted == true && i.Id == "si_premium") == 1 && // Premium item deleted by ID
|
opts.Items.Count(i => i.Id == "si_premium" && i.Price == "teams-seat-annually" && i.Quantity == 1 && i.Deleted != true) == 1 && // Premium item modified
|
||||||
opts.Items.Count(i => i.Id == "si_other_product") == 0 && // Other product NOT in update (untouched)
|
opts.Items.Count(i => i.Id == "si_other_product") == 0)); // Other product NOT in update (untouched)
|
||||||
opts.Items.Any(i => i.Price == "teams-seat-annually" && i.Quantity == 1)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
@ -589,7 +580,7 @@ public class UpgradePremiumToOrganizationCommandTests
|
|||||||
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually);
|
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(result.IsT0);
|
Assert.True(result.IsT0);
|
||||||
@ -598,10 +589,8 @@ public class UpgradePremiumToOrganizationCommandTests
|
|||||||
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
||||||
"sub_123",
|
"sub_123",
|
||||||
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
||||||
opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.PreviousAdditionalStorage) &&
|
opts.Items.Count == 2 && // 1 modified (premium to new price) + 1 deleted (storage)
|
||||||
opts.Metadata[StripeConstants.MetadataKeys.PreviousAdditionalStorage] == "5" &&
|
opts.Items.Count(i => i.Deleted == true) == 1));
|
||||||
opts.Items.Count == 3 && // 2 deleted (premium + storage) + 1 new seat
|
|
||||||
opts.Items.Count(i => i.Deleted == true) == 2));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
@ -636,11 +625,385 @@ public class UpgradePremiumToOrganizationCommandTests
|
|||||||
_pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);
|
_pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually);
|
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(result.IsT1);
|
Assert.True(result.IsT1);
|
||||||
var badRequest = result.AsT1;
|
var badRequest = result.AsT1;
|
||||||
Assert.Equal("Premium subscription item not found.", badRequest.Response);
|
Assert.Equal("Premium subscription password manager item not found.", badRequest.Response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_UpdatesCustomerBillingAddress(User user)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Premium = true;
|
||||||
|
user.GatewaySubscriptionId = "sub_123";
|
||||||
|
user.GatewayCustomerId = "cus_123";
|
||||||
|
|
||||||
|
var mockSubscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = "sub_123",
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new SubscriptionItem
|
||||||
|
{
|
||||||
|
Id = "si_premium",
|
||||||
|
Price = new Price { Id = "premium-annually" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Metadata = new Dictionary<string, string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var mockPremiumPlans = CreateTestPremiumPlansList();
|
||||||
|
var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: "teams-seat-annually");
|
||||||
|
|
||||||
|
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(mockSubscription);
|
||||||
|
_pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);
|
||||||
|
_stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);
|
||||||
|
_stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));
|
||||||
|
_organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));
|
||||||
|
_organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));
|
||||||
|
_organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));
|
||||||
|
_applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);
|
||||||
|
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var billingAddress = new Core.Billing.Payment.Models.BillingAddress { Country = "US", PostalCode = "12345" };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, billingAddress);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsT0);
|
||||||
|
|
||||||
|
await _stripeAdapter.Received(1).UpdateCustomerAsync(
|
||||||
|
"cus_123",
|
||||||
|
Arg.Is<CustomerUpdateOptions>(opts =>
|
||||||
|
opts.Address.Country == "US" &&
|
||||||
|
opts.Address.PostalCode == "12345"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_EnablesAutomaticTaxOnSubscription(User user)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Premium = true;
|
||||||
|
user.GatewaySubscriptionId = "sub_123";
|
||||||
|
user.GatewayCustomerId = "cus_123";
|
||||||
|
|
||||||
|
var mockSubscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = "sub_123",
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new SubscriptionItem
|
||||||
|
{
|
||||||
|
Id = "si_premium",
|
||||||
|
Price = new Price { Id = "premium-annually" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Metadata = new Dictionary<string, string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var mockPremiumPlans = CreateTestPremiumPlansList();
|
||||||
|
var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: "teams-seat-annually");
|
||||||
|
|
||||||
|
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(mockSubscription);
|
||||||
|
_pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);
|
||||||
|
_stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);
|
||||||
|
_stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));
|
||||||
|
_organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));
|
||||||
|
_organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));
|
||||||
|
_organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));
|
||||||
|
_applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);
|
||||||
|
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsT0);
|
||||||
|
|
||||||
|
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
||||||
|
"sub_123",
|
||||||
|
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
||||||
|
opts.AutomaticTax != null &&
|
||||||
|
opts.AutomaticTax.Enabled == true));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_UsesAlwaysInvoiceProrationBehavior(User user)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Premium = true;
|
||||||
|
user.GatewaySubscriptionId = "sub_123";
|
||||||
|
user.GatewayCustomerId = "cus_123";
|
||||||
|
|
||||||
|
var mockSubscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = "sub_123",
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new SubscriptionItem
|
||||||
|
{
|
||||||
|
Id = "si_premium",
|
||||||
|
Price = new Price { Id = "premium-annually" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Metadata = new Dictionary<string, string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var mockPremiumPlans = CreateTestPremiumPlansList();
|
||||||
|
var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: "teams-seat-annually");
|
||||||
|
|
||||||
|
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(mockSubscription);
|
||||||
|
_pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);
|
||||||
|
_stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);
|
||||||
|
_stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));
|
||||||
|
_organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));
|
||||||
|
_organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));
|
||||||
|
_organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));
|
||||||
|
_applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);
|
||||||
|
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsT0);
|
||||||
|
|
||||||
|
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
||||||
|
"sub_123",
|
||||||
|
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
||||||
|
opts.ProrationBehavior == "always_invoice"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_ModifiesExistingSubscriptionItem_NotDeleteAndRecreate(User user)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Premium = true;
|
||||||
|
user.GatewaySubscriptionId = "sub_123";
|
||||||
|
user.GatewayCustomerId = "cus_123";
|
||||||
|
|
||||||
|
var mockSubscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = "sub_123",
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new SubscriptionItem
|
||||||
|
{
|
||||||
|
Id = "si_premium",
|
||||||
|
Price = new Price { Id = "premium-annually" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Metadata = new Dictionary<string, string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var mockPremiumPlans = CreateTestPremiumPlansList();
|
||||||
|
var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: "teams-seat-annually");
|
||||||
|
|
||||||
|
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(mockSubscription);
|
||||||
|
_pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);
|
||||||
|
_stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);
|
||||||
|
_stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));
|
||||||
|
_organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));
|
||||||
|
_organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));
|
||||||
|
_organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));
|
||||||
|
_applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);
|
||||||
|
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsT0);
|
||||||
|
|
||||||
|
// Verify that the subscription item was modified, not deleted
|
||||||
|
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
||||||
|
"sub_123",
|
||||||
|
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
||||||
|
// Should have an item with the original ID being modified
|
||||||
|
opts.Items.Any(item =>
|
||||||
|
item.Id == "si_premium" &&
|
||||||
|
item.Price == "teams-seat-annually" &&
|
||||||
|
item.Quantity == 1 &&
|
||||||
|
item.Deleted != true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_CreatesOrganizationWithCorrectSettings(User user)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Premium = true;
|
||||||
|
user.GatewaySubscriptionId = "sub_123";
|
||||||
|
user.GatewayCustomerId = "cus_123";
|
||||||
|
|
||||||
|
var mockSubscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = "sub_123",
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new SubscriptionItem
|
||||||
|
{
|
||||||
|
Id = "si_premium",
|
||||||
|
Price = new Price { Id = "premium-annually" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Metadata = new Dictionary<string, string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var mockPremiumPlans = CreateTestPremiumPlansList();
|
||||||
|
var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: "teams-seat-annually");
|
||||||
|
|
||||||
|
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(mockSubscription);
|
||||||
|
_pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);
|
||||||
|
_stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);
|
||||||
|
_stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));
|
||||||
|
_organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));
|
||||||
|
_organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));
|
||||||
|
_organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));
|
||||||
|
_applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);
|
||||||
|
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsT0);
|
||||||
|
|
||||||
|
await _organizationRepository.Received(1).CreateAsync(
|
||||||
|
Arg.Is<Organization>(org =>
|
||||||
|
org.Name == "My Organization" &&
|
||||||
|
org.BillingEmail == user.Email &&
|
||||||
|
org.PlanType == PlanType.TeamsAnnually &&
|
||||||
|
org.Seats == 1 &&
|
||||||
|
org.Gateway == GatewayType.Stripe &&
|
||||||
|
org.GatewayCustomerId == "cus_123" &&
|
||||||
|
org.GatewaySubscriptionId == "sub_123" &&
|
||||||
|
org.Enabled == true));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_CreatesOrganizationApiKeyWithCorrectType(User user)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Premium = true;
|
||||||
|
user.GatewaySubscriptionId = "sub_123";
|
||||||
|
user.GatewayCustomerId = "cus_123";
|
||||||
|
|
||||||
|
var mockSubscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = "sub_123",
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new SubscriptionItem
|
||||||
|
{
|
||||||
|
Id = "si_premium",
|
||||||
|
Price = new Price { Id = "premium-annually" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Metadata = new Dictionary<string, string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var mockPremiumPlans = CreateTestPremiumPlansList();
|
||||||
|
var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: "teams-seat-annually");
|
||||||
|
|
||||||
|
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(mockSubscription);
|
||||||
|
_pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);
|
||||||
|
_stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);
|
||||||
|
_stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));
|
||||||
|
_organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));
|
||||||
|
_organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));
|
||||||
|
_organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));
|
||||||
|
_applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);
|
||||||
|
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsT0);
|
||||||
|
|
||||||
|
await _organizationApiKeyRepository.Received(1).CreateAsync(
|
||||||
|
Arg.Is<OrganizationApiKey>(apiKey =>
|
||||||
|
apiKey.Type == OrganizationApiKeyType.Default &&
|
||||||
|
!string.IsNullOrEmpty(apiKey.ApiKey)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_CreatesOrganizationUserAsOwnerWithAllPermissions(User user)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.Premium = true;
|
||||||
|
user.GatewaySubscriptionId = "sub_123";
|
||||||
|
user.GatewayCustomerId = "cus_123";
|
||||||
|
|
||||||
|
var mockSubscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = "sub_123",
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new SubscriptionItem
|
||||||
|
{
|
||||||
|
Id = "si_premium",
|
||||||
|
Price = new Price { Id = "premium-annually" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Metadata = new Dictionary<string, string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var mockPremiumPlans = CreateTestPremiumPlansList();
|
||||||
|
var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: "teams-seat-annually");
|
||||||
|
|
||||||
|
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(mockSubscription);
|
||||||
|
_pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);
|
||||||
|
_stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);
|
||||||
|
_stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));
|
||||||
|
_organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));
|
||||||
|
_organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));
|
||||||
|
_organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));
|
||||||
|
_applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);
|
||||||
|
_userService.SaveUserAsync(user).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _command.Run(user, "My Organization", "encrypted-key", PlanType.TeamsAnnually, CreateTestBillingAddress());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.IsT0);
|
||||||
|
|
||||||
|
await _organizationUserRepository.Received(1).CreateAsync(
|
||||||
|
Arg.Is<OrganizationUser>(orgUser =>
|
||||||
|
orgUser.UserId == user.Id &&
|
||||||
|
orgUser.Type == OrganizationUserType.Owner &&
|
||||||
|
orgUser.Status == OrganizationUserStatusType.Confirmed));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -9,6 +12,7 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||||
using Bit.Core.Test.Billing.Mocks;
|
using Bit.Core.Test.Billing.Mocks;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
@ -72,8 +76,12 @@ public class UpgradeOrganizationPlanCommandTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[FreeOrganizationUpgradeCustomize, BitAutoData]
|
[FreeOrganizationUpgradeCustomize, BitAutoData]
|
||||||
public async Task UpgradePlan_Passes(Organization organization, OrganizationUpgrade upgrade,
|
public async Task UpgradePlan_Passes(Organization organization, OrganizationUpgrade upgrade,
|
||||||
|
[Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,
|
||||||
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
||||||
{
|
{
|
||||||
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
|
.RunAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>())
|
||||||
|
.Returns(policy);
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));
|
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));
|
||||||
upgrade.AdditionalSmSeats = 10;
|
upgrade.AdditionalSmSeats = 10;
|
||||||
@ -100,6 +108,7 @@ public class UpgradeOrganizationPlanCommandTests
|
|||||||
PlanType planType,
|
PlanType planType,
|
||||||
Organization organization,
|
Organization organization,
|
||||||
OrganizationUpgrade organizationUpgrade,
|
OrganizationUpgrade organizationUpgrade,
|
||||||
|
[Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,
|
||||||
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
@ -116,6 +125,9 @@ public class UpgradeOrganizationPlanCommandTests
|
|||||||
organizationUpgrade.Plan = planType;
|
organizationUpgrade.Plan = planType;
|
||||||
|
|
||||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organizationUpgrade.Plan).Returns(MockPlans.Get(organizationUpgrade.Plan));
|
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organizationUpgrade.Plan).Returns(MockPlans.Get(organizationUpgrade.Plan));
|
||||||
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
|
.RunAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>())
|
||||||
|
.Returns(policy);
|
||||||
sutProvider.GetDependency<IOrganizationRepository>()
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||||
{
|
{
|
||||||
@ -141,15 +153,20 @@ public class UpgradeOrganizationPlanCommandTests
|
|||||||
[BitAutoData(PlanType.TeamsAnnually)]
|
[BitAutoData(PlanType.TeamsAnnually)]
|
||||||
[BitAutoData(PlanType.TeamsStarter)]
|
[BitAutoData(PlanType.TeamsStarter)]
|
||||||
public async Task UpgradePlan_SM_Passes(PlanType planType, Organization organization, OrganizationUpgrade upgrade,
|
public async Task UpgradePlan_SM_Passes(PlanType planType, Organization organization, OrganizationUpgrade upgrade,
|
||||||
|
[Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,
|
||||||
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));
|
|
||||||
|
|
||||||
upgrade.Plan = planType;
|
upgrade.Plan = planType;
|
||||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(MockPlans.Get(upgrade.Plan));
|
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(MockPlans.Get(upgrade.Plan));
|
||||||
|
|
||||||
var plan = MockPlans.Get(upgrade.Plan);
|
var plan = MockPlans.Get(upgrade.Plan);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
|
.RunAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>())
|
||||||
|
.Returns(policy);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
|
||||||
upgrade.AdditionalSeats = 15;
|
upgrade.AdditionalSeats = 15;
|
||||||
@ -180,6 +197,7 @@ public class UpgradeOrganizationPlanCommandTests
|
|||||||
[BitAutoData(PlanType.TeamsAnnually)]
|
[BitAutoData(PlanType.TeamsAnnually)]
|
||||||
[BitAutoData(PlanType.TeamsStarter)]
|
[BitAutoData(PlanType.TeamsStarter)]
|
||||||
public async Task UpgradePlan_SM_NotEnoughSmSeats_Throws(PlanType planType, Organization organization, OrganizationUpgrade upgrade,
|
public async Task UpgradePlan_SM_NotEnoughSmSeats_Throws(PlanType planType, Organization organization, OrganizationUpgrade upgrade,
|
||||||
|
[Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,
|
||||||
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
||||||
{
|
{
|
||||||
upgrade.Plan = planType;
|
upgrade.Plan = planType;
|
||||||
@ -191,6 +209,10 @@ public class UpgradeOrganizationPlanCommandTests
|
|||||||
organization.SmSeats = 2;
|
organization.SmSeats = 2;
|
||||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));
|
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
|
.RunAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>())
|
||||||
|
.Returns(policy);
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
sutProvider.GetDependency<IOrganizationRepository>()
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||||
@ -214,7 +236,9 @@ public class UpgradeOrganizationPlanCommandTests
|
|||||||
[BitAutoData(PlanType.TeamsAnnually, 51)]
|
[BitAutoData(PlanType.TeamsAnnually, 51)]
|
||||||
[BitAutoData(PlanType.TeamsStarter, 51)]
|
[BitAutoData(PlanType.TeamsStarter, 51)]
|
||||||
public async Task UpgradePlan_SM_NotEnoughServiceAccounts_Throws(PlanType planType, int currentServiceAccounts,
|
public async Task UpgradePlan_SM_NotEnoughServiceAccounts_Throws(PlanType planType, int currentServiceAccounts,
|
||||||
Organization organization, OrganizationUpgrade upgrade, SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
Organization organization, OrganizationUpgrade upgrade,
|
||||||
|
[Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,
|
||||||
|
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
||||||
{
|
{
|
||||||
upgrade.Plan = planType;
|
upgrade.Plan = planType;
|
||||||
upgrade.AdditionalSeats = 15;
|
upgrade.AdditionalSeats = 15;
|
||||||
@ -226,6 +250,10 @@ public class UpgradeOrganizationPlanCommandTests
|
|||||||
organization.SmServiceAccounts = currentServiceAccounts;
|
organization.SmServiceAccounts = currentServiceAccounts;
|
||||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));
|
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
|
.RunAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>())
|
||||||
|
.Returns(policy);
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
sutProvider.GetDependency<IOrganizationRepository>()
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||||
@ -251,6 +279,7 @@ public class UpgradeOrganizationPlanCommandTests
|
|||||||
OrganizationUpgrade upgrade,
|
OrganizationUpgrade upgrade,
|
||||||
string newPublicKey,
|
string newPublicKey,
|
||||||
string newPrivateKey,
|
string newPrivateKey,
|
||||||
|
[Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,
|
||||||
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
||||||
{
|
{
|
||||||
organization.PublicKey = null;
|
organization.PublicKey = null;
|
||||||
@ -262,6 +291,9 @@ public class UpgradeOrganizationPlanCommandTests
|
|||||||
publicKey: newPublicKey);
|
publicKey: newPublicKey);
|
||||||
upgrade.AdditionalSeats = 10;
|
upgrade.AdditionalSeats = 10;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
|
.RunAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>())
|
||||||
|
.Returns(policy);
|
||||||
sutProvider.GetDependency<IOrganizationRepository>()
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
.GetByIdAsync(organization.Id)
|
.GetByIdAsync(organization.Id)
|
||||||
.Returns(organization);
|
.Returns(organization);
|
||||||
@ -291,6 +323,7 @@ public class UpgradeOrganizationPlanCommandTests
|
|||||||
public async Task UpgradePlan_WhenOrganizationAlreadyHasPublicAndPrivateKeys_DoesNotOverwriteWithNull(
|
public async Task UpgradePlan_WhenOrganizationAlreadyHasPublicAndPrivateKeys_DoesNotOverwriteWithNull(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
OrganizationUpgrade upgrade,
|
OrganizationUpgrade upgrade,
|
||||||
|
[Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,
|
||||||
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@ -304,6 +337,9 @@ public class UpgradeOrganizationPlanCommandTests
|
|||||||
upgrade.Keys = null;
|
upgrade.Keys = null;
|
||||||
upgrade.AdditionalSeats = 10;
|
upgrade.AdditionalSeats = 10;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
|
.RunAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>())
|
||||||
|
.Returns(policy);
|
||||||
sutProvider.GetDependency<IOrganizationRepository>()
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
.GetByIdAsync(organization.Id)
|
.GetByIdAsync(organization.Id)
|
||||||
.Returns(organization);
|
.Returns(organization);
|
||||||
@ -333,6 +369,7 @@ public class UpgradeOrganizationPlanCommandTests
|
|||||||
public async Task UpgradePlan_WhenOrganizationAlreadyHasPublicAndPrivateKeys_DoesNotBackfillWithNewKeys(
|
public async Task UpgradePlan_WhenOrganizationAlreadyHasPublicAndPrivateKeys_DoesNotBackfillWithNewKeys(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
OrganizationUpgrade upgrade,
|
OrganizationUpgrade upgrade,
|
||||||
|
[Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,
|
||||||
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@ -343,6 +380,9 @@ public class UpgradeOrganizationPlanCommandTests
|
|||||||
|
|
||||||
organization.PublicKey = existingPublicKey;
|
organization.PublicKey = existingPublicKey;
|
||||||
organization.PrivateKey = existingPrivateKey;
|
organization.PrivateKey = existingPrivateKey;
|
||||||
|
sutProvider.GetDependency<IPolicyQuery>()
|
||||||
|
.RunAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>())
|
||||||
|
.Returns(policy);
|
||||||
|
|
||||||
upgrade.Plan = PlanType.TeamsAnnually;
|
upgrade.Plan = PlanType.TeamsAnnually;
|
||||||
upgrade.Keys = new PublicKeyEncryptionKeyPairData(
|
upgrade.Keys = new PublicKeyEncryptionKeyPairData(
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using NSubstitute.ReturnsExtensions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.OrganizationFeatures.Policies;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class PolicyQueryTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RunAsync_WithExistingPolicy_ReturnsPolicy(SutProvider<PolicyQuery> sutProvider,
|
||||||
|
Policy policy)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IPolicyRepository>()
|
||||||
|
.GetByOrganizationIdTypeAsync(policy.OrganizationId, policy.Type)
|
||||||
|
.Returns(policy);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var policyData = await sutProvider.Sut.RunAsync(policy.OrganizationId, policy.Type);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(policy.Data, policyData.Data);
|
||||||
|
Assert.Equal(policy.Type, policyData.Type);
|
||||||
|
Assert.Equal(policy.Enabled, policyData.Enabled);
|
||||||
|
Assert.Equal(policy.OrganizationId, policyData.OrganizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RunAsync_WithNonExistentPolicy_ReturnsDefaultDisabledPolicy(
|
||||||
|
SutProvider<PolicyQuery> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
PolicyType policyType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IPolicyRepository>()
|
||||||
|
.GetByOrganizationIdTypeAsync(organizationId, policyType)
|
||||||
|
.ReturnsNull();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var policyData = await sutProvider.Sut.RunAsync(organizationId, policyType);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(organizationId, policyData.OrganizationId);
|
||||||
|
Assert.Equal(policyType, policyData.Type);
|
||||||
|
Assert.False(policyData.Enabled);
|
||||||
|
Assert.Null(policyData.Data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -254,21 +254,6 @@ public class HandlebarsMailServiceTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task SendSendEmailOtpEmailAsync_SendsEmail()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var email = "test@example.com";
|
|
||||||
var token = "aToken";
|
|
||||||
var subject = string.Format("Your Bitwarden Send verification code is {0}", token);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await _sut.SendSendEmailOtpEmailAsync(email, token, subject);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Any<MailMessage>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SendIndividualUserWelcomeEmailAsync_SendsCorrectEmail()
|
public async Task SendIndividualUserWelcomeEmailAsync_SendsCorrectEmail()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -326,4 +326,101 @@ public class ImportCiphersAsyncCommandTests
|
|||||||
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ImportIntoIndividualVaultAsync_WithArchivedCiphers_PreservesArchiveStatus(
|
||||||
|
Guid importingUserId,
|
||||||
|
List<CipherDetails> ciphers,
|
||||||
|
SutProvider<ImportCiphersCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var archivedDate = DateTime.UtcNow.AddDays(-1);
|
||||||
|
ciphers[0].UserId = importingUserId;
|
||||||
|
ciphers[0].ArchivedDate = archivedDate;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.OrganizationDataOwnership)
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFolderRepository>()
|
||||||
|
.GetManyByUserIdAsync(importingUserId)
|
||||||
|
.Returns(new List<Folder>());
|
||||||
|
|
||||||
|
var folders = new List<Folder>();
|
||||||
|
var folderRelationships = new List<KeyValuePair<int, int>>();
|
||||||
|
|
||||||
|
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ICipherRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.CreateAsync(importingUserId,
|
||||||
|
Arg.Is<List<CipherDetails>>(c =>
|
||||||
|
c[0].Archives != null &&
|
||||||
|
c[0].Archives.Contains(importingUserId.ToString().ToUpperInvariant()) &&
|
||||||
|
c[0].Archives.Contains(archivedDate.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ"))),
|
||||||
|
Arg.Any<List<Folder>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Archive functionality is a per-user function. When importing archived ciphers into an organization vault,
|
||||||
|
* the Archives field should be set for the importing user only. This allows the importing user to see
|
||||||
|
* items as archived, while other organization members will not see them as archived.
|
||||||
|
*/
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ImportIntoOrganizationalVaultAsync_WithArchivedCiphers_SetsArchivesForImportingUserOnly(
|
||||||
|
Organization organization,
|
||||||
|
Guid importingUserId,
|
||||||
|
OrganizationUser importingOrganizationUser,
|
||||||
|
List<Collection> collections,
|
||||||
|
List<CipherDetails> ciphers,
|
||||||
|
SutProvider<ImportCiphersCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var archivedDate = DateTime.UtcNow.AddDays(-1);
|
||||||
|
organization.MaxCollections = null;
|
||||||
|
importingOrganizationUser.OrganizationId = organization.Id;
|
||||||
|
|
||||||
|
foreach (var collection in collections)
|
||||||
|
{
|
||||||
|
collection.OrganizationId = organization.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var cipher in ciphers)
|
||||||
|
{
|
||||||
|
cipher.OrganizationId = organization.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphers[0].ArchivedDate = archivedDate;
|
||||||
|
ciphers[0].Archives = null;
|
||||||
|
|
||||||
|
KeyValuePair<int, int>[] collectionRelationships = {
|
||||||
|
new(0, 0),
|
||||||
|
new(1, 1),
|
||||||
|
new(2, 2)
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetByOrganizationAsync(organization.Id, importingUserId)
|
||||||
|
.Returns(importingOrganizationUser);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>()
|
||||||
|
.GetManyByOrganizationIdAsync(organization.Id)
|
||||||
|
.Returns(new List<Collection>());
|
||||||
|
|
||||||
|
await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ICipherRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.CreateAsync(
|
||||||
|
Arg.Is<List<CipherDetails>>(c =>
|
||||||
|
c[0].ArchivedDate == archivedDate &&
|
||||||
|
c[0].Archives != null &&
|
||||||
|
c[0].Archives.Contains(importingUserId.ToString().ToUpperInvariant()) &&
|
||||||
|
c[0].Archives.Contains(archivedDate.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ"))),
|
||||||
|
Arg.Any<IEnumerable<Collection>>(),
|
||||||
|
Arg.Any<IEnumerable<CollectionCipher>>(),
|
||||||
|
Arg.Any<IEnumerable<CollectionUser>>());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,7 +56,7 @@ public class SendAuthenticationQueryTests
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var emailOtp = Assert.IsType<EmailOtp>(result);
|
var emailOtp = Assert.IsType<EmailOtp>(result);
|
||||||
Assert.Equal(expectedEmailHashes, emailOtp.Emails);
|
Assert.Equal(expectedEmailHashes, emailOtp.EmailHashes);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user