mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 13:08:17 -05:00
Merge branch 'main' into add-docker-arm64-builds
This commit is contained in:
commit
6284df7aa9
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -202,6 +202,8 @@ jobs:
|
|||||||
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
||||||
with:
|
with:
|
||||||
sarif_file: ${{ steps.container-scan.outputs.sarif }}
|
sarif_file: ${{ steps.container-scan.outputs.sarif }}
|
||||||
|
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
|
||||||
|
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
||||||
|
|
||||||
build-stub-swagger:
|
build-stub-swagger:
|
||||||
name: Build Docker-Stub/Swagger
|
name: Build Docker-Stub/Swagger
|
||||||
|
2
.github/workflows/scan.yml
vendored
2
.github/workflows/scan.yml
vendored
@ -49,6 +49,8 @@ jobs:
|
|||||||
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
||||||
with:
|
with:
|
||||||
sarif_file: cx_result.sarif
|
sarif_file: cx_result.sarif
|
||||||
|
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
|
||||||
|
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
||||||
|
|
||||||
quality:
|
quality:
|
||||||
name: Quality scan
|
name: Quality scan
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
<Version>2025.3.0</Version>
|
<Version>2025.3.3</Version>
|
||||||
|
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
@ -8,6 +8,8 @@ using Bit.Core.AdminConsole.Enums;
|
|||||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||||
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.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
@ -55,8 +57,10 @@ public class OrganizationUsersController : Controller
|
|||||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||||
private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand;
|
private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand;
|
||||||
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
|
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
|
||||||
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
|
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
|
||||||
|
|
||||||
public OrganizationUsersController(
|
public OrganizationUsersController(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@ -79,8 +83,10 @@ public class OrganizationUsersController : Controller
|
|||||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||||
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand,
|
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand,
|
||||||
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
|
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
|
||||||
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IPricingClient pricingClient)
|
IPricingClient pricingClient,
|
||||||
|
IConfirmOrganizationUserCommand confirmOrganizationUserCommand)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@ -102,8 +108,10 @@ public class OrganizationUsersController : Controller
|
|||||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||||
_deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand;
|
_deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand;
|
||||||
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
|
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
|
||||||
|
_policyRequirementQuery = policyRequirementQuery;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
|
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
@ -303,7 +311,7 @@ public class OrganizationUsersController : Controller
|
|||||||
|
|
||||||
await _organizationService.InitPendingOrganization(user.Id, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName);
|
await _organizationService.InitPendingOrganization(user.Id, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName);
|
||||||
await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService);
|
await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService);
|
||||||
await _organizationService.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id);
|
await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{organizationUserId}/accept")]
|
[HttpPost("{organizationUserId}/accept")]
|
||||||
@ -315,11 +323,13 @@ public class OrganizationUsersController : Controller
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var useMasterPasswordPolicy = await ShouldHandleResetPasswordAsync(orgId);
|
var useMasterPasswordPolicy = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||||
|
? (await _policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id)).AutoEnrollEnabled(orgId)
|
||||||
|
: await ShouldHandleResetPasswordAsync(orgId);
|
||||||
|
|
||||||
if (useMasterPasswordPolicy && string.IsNullOrWhiteSpace(model.ResetPasswordKey))
|
if (useMasterPasswordPolicy && string.IsNullOrWhiteSpace(model.ResetPasswordKey))
|
||||||
{
|
{
|
||||||
throw new BadRequestException(string.Empty, "Master Password reset is required, but not provided.");
|
throw new BadRequestException("Master Password reset is required, but not provided.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService);
|
await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService);
|
||||||
@ -357,7 +367,7 @@ public class OrganizationUsersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var userId = _userService.GetProperUserId(User);
|
var userId = _userService.GetProperUserId(User);
|
||||||
var result = await _organizationService.ConfirmUserAsync(orgGuidId, new Guid(id), model.Key, userId.Value);
|
var result = await _confirmOrganizationUserCommand.ConfirmUserAsync(orgGuidId, new Guid(id), model.Key, userId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("confirm")]
|
[HttpPost("confirm")]
|
||||||
@ -371,7 +381,7 @@ public class OrganizationUsersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var userId = _userService.GetProperUserId(User);
|
var userId = _userService.GetProperUserId(User);
|
||||||
var results = await _organizationService.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value);
|
var results = await _confirmOrganizationUserCommand.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value);
|
||||||
|
|
||||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>
|
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>
|
||||||
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
|
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
|
||||||
|
@ -16,6 +16,8 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||||
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.PolicyRequirements;
|
||||||
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;
|
||||||
@ -61,6 +63,7 @@ public class OrganizationsController : Controller
|
|||||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||||
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
|
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
|
||||||
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
|
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
|
||||||
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
|
|
||||||
public OrganizationsController(
|
public OrganizationsController(
|
||||||
@ -84,6 +87,7 @@ public class OrganizationsController : Controller
|
|||||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||||
ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand,
|
ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand,
|
||||||
IOrganizationDeleteCommand organizationDeleteCommand,
|
IOrganizationDeleteCommand organizationDeleteCommand,
|
||||||
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
IPricingClient pricingClient)
|
IPricingClient pricingClient)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
@ -106,6 +110,7 @@ public class OrganizationsController : Controller
|
|||||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||||
_cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand;
|
_cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand;
|
||||||
_organizationDeleteCommand = organizationDeleteCommand;
|
_organizationDeleteCommand = organizationDeleteCommand;
|
||||||
|
_policyRequirementQuery = policyRequirementQuery;
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,8 +168,13 @@ public class OrganizationsController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var resetPasswordPolicy =
|
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
|
||||||
await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
|
{
|
||||||
|
var resetPasswordPolicyRequirement = await _policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||||
|
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, resetPasswordPolicyRequirement.AutoEnrollEnabled(organization.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
var resetPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
|
||||||
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled || resetPasswordPolicy.Data == null)
|
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled || resetPasswordPolicy.Data == null)
|
||||||
{
|
{
|
||||||
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, false);
|
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, false);
|
||||||
@ -172,6 +182,7 @@ public class OrganizationsController : Controller
|
|||||||
|
|
||||||
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("")]
|
||||||
|
@ -127,9 +127,9 @@ public class CiphersController : Controller
|
|||||||
public async Task<ListResponseModel<CipherDetailsResponseModel>> Get()
|
public async Task<ListResponseModel<CipherDetailsResponseModel>> Get()
|
||||||
{
|
{
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
var hasOrgs = _currentContext.Organizations?.Any() ?? false;
|
var hasOrgs = _currentContext.Organizations.Count != 0;
|
||||||
// TODO: Use hasOrgs proper for cipher listing here?
|
// TODO: Use hasOrgs proper for cipher listing here?
|
||||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: true || hasOrgs);
|
var ciphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: true);
|
||||||
Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict = null;
|
Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict = null;
|
||||||
if (hasOrgs)
|
if (hasOrgs)
|
||||||
{
|
{
|
||||||
|
@ -3,8 +3,6 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<UserSecretsId>bitwarden-Billing</UserSecretsId>
|
<UserSecretsId>bitwarden-Billing</UserSecretsId>
|
||||||
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||||
<!-- Temp exclusions until warnings are fixed -->
|
|
||||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS9113</WarningsNotAsErrors>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Billing' " />
|
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Billing' " />
|
||||||
|
@ -0,0 +1,186 @@
|
|||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Platform.Push;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||||
|
|
||||||
|
public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
||||||
|
{
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly IEventService _eventService;
|
||||||
|
private readonly IMailService _mailService;
|
||||||
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
|
private readonly IPushRegistrationService _pushRegistrationService;
|
||||||
|
private readonly IPolicyService _policyService;
|
||||||
|
private readonly IDeviceRepository _deviceRepository;
|
||||||
|
|
||||||
|
public ConfirmOrganizationUserCommand(
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IEventService eventService,
|
||||||
|
IMailService mailService,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
|
IPushNotificationService pushNotificationService,
|
||||||
|
IPushRegistrationService pushRegistrationService,
|
||||||
|
IPolicyService policyService,
|
||||||
|
IDeviceRepository deviceRepository)
|
||||||
|
{
|
||||||
|
_organizationRepository = organizationRepository;
|
||||||
|
_organizationUserRepository = organizationUserRepository;
|
||||||
|
_userRepository = userRepository;
|
||||||
|
_eventService = eventService;
|
||||||
|
_mailService = mailService;
|
||||||
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
|
_pushNotificationService = pushNotificationService;
|
||||||
|
_pushRegistrationService = pushRegistrationService;
|
||||||
|
_policyService = policyService;
|
||||||
|
_deviceRepository = deviceRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
||||||
|
Guid confirmingUserId)
|
||||||
|
{
|
||||||
|
var result = await ConfirmUsersAsync(
|
||||||
|
organizationId,
|
||||||
|
new Dictionary<Guid, string>() { { organizationUserId, key } },
|
||||||
|
confirmingUserId);
|
||||||
|
|
||||||
|
if (!result.Any())
|
||||||
|
{
|
||||||
|
throw new BadRequestException("User not valid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var (orgUser, error) = result[0];
|
||||||
|
if (error != "")
|
||||||
|
{
|
||||||
|
throw new BadRequestException(error);
|
||||||
|
}
|
||||||
|
return orgUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,
|
||||||
|
Guid confirmingUserId)
|
||||||
|
{
|
||||||
|
var selectedOrganizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys);
|
||||||
|
var validSelectedOrganizationUsers = selectedOrganizationUsers
|
||||||
|
.Where(u => u.Status == OrganizationUserStatusType.Accepted && u.OrganizationId == organizationId && u.UserId != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (!validSelectedOrganizationUsers.Any())
|
||||||
|
{
|
||||||
|
return new List<Tuple<OrganizationUser, string>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var validSelectedUserIds = validSelectedOrganizationUsers.Select(u => u.UserId.Value).ToList();
|
||||||
|
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
var allUsersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validSelectedUserIds);
|
||||||
|
var users = await _userRepository.GetManyAsync(validSelectedUserIds);
|
||||||
|
var usersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(validSelectedUserIds);
|
||||||
|
|
||||||
|
var keyedFilteredUsers = validSelectedOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u);
|
||||||
|
var keyedOrganizationUsers = allUsersOrgs.GroupBy(u => u.UserId.Value)
|
||||||
|
.ToDictionary(u => u.Key, u => u.ToList());
|
||||||
|
|
||||||
|
var succeededUsers = new List<OrganizationUser>();
|
||||||
|
var result = new List<Tuple<OrganizationUser, string>>();
|
||||||
|
|
||||||
|
foreach (var user in users)
|
||||||
|
{
|
||||||
|
if (!keyedFilteredUsers.ContainsKey(user.Id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var orgUser = keyedFilteredUsers[user.Id];
|
||||||
|
var orgUsers = keyedOrganizationUsers.GetValueOrDefault(user.Id, new List<OrganizationUser>());
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (organization.PlanType == PlanType.Free && (orgUser.Type == OrganizationUserType.Admin
|
||||||
|
|| orgUser.Type == OrganizationUserType.Owner))
|
||||||
|
{
|
||||||
|
// Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this.
|
||||||
|
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(user.Id);
|
||||||
|
if (adminCount > 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("User can only be an admin of one free organization.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var twoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;
|
||||||
|
await CheckPoliciesAsync(organizationId, user, orgUsers, twoFactorEnabled);
|
||||||
|
orgUser.Status = OrganizationUserStatusType.Confirmed;
|
||||||
|
orgUser.Key = keys[orgUser.Id];
|
||||||
|
orgUser.Email = null;
|
||||||
|
|
||||||
|
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||||
|
await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, orgUser.AccessSecretsManager);
|
||||||
|
await DeleteAndPushUserRegistrationAsync(organizationId, user.Id);
|
||||||
|
succeededUsers.Add(orgUser);
|
||||||
|
result.Add(Tuple.Create(orgUser, ""));
|
||||||
|
}
|
||||||
|
catch (BadRequestException e)
|
||||||
|
{
|
||||||
|
result.Add(Tuple.Create(orgUser, e.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _organizationUserRepository.ReplaceManyAsync(succeededUsers);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CheckPoliciesAsync(Guid organizationId, User user,
|
||||||
|
ICollection<OrganizationUser> userOrgs, bool twoFactorEnabled)
|
||||||
|
{
|
||||||
|
// Enforce Two Factor Authentication Policy for this organization
|
||||||
|
var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication))
|
||||||
|
.Any(p => p.OrganizationId == organizationId);
|
||||||
|
if (orgRequiresTwoFactor && !twoFactorEnabled)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("User does not have two-step login enabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId);
|
||||||
|
var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);
|
||||||
|
var otherSingleOrgPolicies =
|
||||||
|
singleOrgPolicies.Where(p => p.OrganizationId != organizationId);
|
||||||
|
// Enforce Single Organization Policy for this organization
|
||||||
|
if (hasOtherOrgs && singleOrgPolicies.Any(p => p.OrganizationId == organizationId))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot confirm this member to the organization until they leave or remove all other organizations.");
|
||||||
|
}
|
||||||
|
// Enforce Single Organization Policy of other organizations user is a member of
|
||||||
|
if (otherSingleOrgPolicies.Any())
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot confirm this member to the organization because they are in another organization which forbids it.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
|
||||||
|
{
|
||||||
|
var devices = await GetUserDeviceIdsAsync(userId);
|
||||||
|
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices,
|
||||||
|
organizationId.ToString());
|
||||||
|
await _pushNotificationService.PushSyncOrgKeysAsync(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
|
||||||
|
{
|
||||||
|
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
||||||
|
return devices
|
||||||
|
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
||||||
|
.Select(d => d.Id.ToString());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command to confirm organization users who have accepted their invitations.
|
||||||
|
/// </summary>
|
||||||
|
public interface IConfirmOrganizationUserCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Confirms a single organization user who has accepted their invitation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationId">The ID of the organization.</param>
|
||||||
|
/// <param name="organizationUserId">The ID of the organization user to confirm.</param>
|
||||||
|
/// <param name="key">The encrypted organization key for the user.</param>
|
||||||
|
/// <param name="confirmingUserId">The ID of the user performing the confirmation.</param>
|
||||||
|
/// <returns>The confirmed organization user.</returns>
|
||||||
|
/// <exception cref="BadRequestException">Thrown when the user is not valid or cannot be confirmed.</exception>
|
||||||
|
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Confirms multiple organization users who have accepted their invitations.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationId">The ID of the organization.</param>
|
||||||
|
/// <param name="keys">A dictionary mapping organization user IDs to their encrypted organization keys.</param>
|
||||||
|
/// <param name="confirmingUserId">The ID of the user performing the confirmation.</param>
|
||||||
|
/// <returns>A list of tuples containing the organization user and an error message (if any).</returns>
|
||||||
|
Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,
|
||||||
|
Guid confirmingUserId);
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Policy requirements for the Account recovery administration policy.
|
||||||
|
/// </summary>
|
||||||
|
public class ResetPasswordPolicyRequirement : IPolicyRequirement
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// List of Organization Ids that require automatic enrollment in password recovery.
|
||||||
|
/// </summary>
|
||||||
|
private IEnumerable<Guid> _autoEnrollOrganizations;
|
||||||
|
public IEnumerable<Guid> AutoEnrollOrganizations { init => _autoEnrollOrganizations = value; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if provided organizationId requires automatic enrollment in password recovery.
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoEnrollEnabled(Guid organizationId)
|
||||||
|
{
|
||||||
|
return _autoEnrollOrganizations.Contains(organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ResetPasswordPolicyRequirementFactory : BasePolicyRequirementFactory<ResetPasswordPolicyRequirement>
|
||||||
|
{
|
||||||
|
public override PolicyType PolicyType => PolicyType.ResetPassword;
|
||||||
|
|
||||||
|
protected override bool ExemptProviders => false;
|
||||||
|
|
||||||
|
protected override IEnumerable<OrganizationUserType> ExemptRoles => [];
|
||||||
|
|
||||||
|
public override ResetPasswordPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
||||||
|
{
|
||||||
|
var result = policyDetails
|
||||||
|
.Where(p => p.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled)
|
||||||
|
.Select(p => p.OrganizationId)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
return new ResetPasswordPolicyRequirement() { AutoEnrollOrganizations = result };
|
||||||
|
}
|
||||||
|
}
|
@ -33,5 +33,6 @@ public static class PolicyServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, DisableSendPolicyRequirementFactory>();
|
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, DisableSendPolicyRequirementFactory>();
|
||||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SendOptionsPolicyRequirementFactory>();
|
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SendOptionsPolicyRequirementFactory>();
|
||||||
|
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, ResetPasswordPolicyRequirementFactory>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,9 +38,6 @@ public interface IOrganizationService
|
|||||||
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites);
|
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites);
|
||||||
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
|
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
|
||||||
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);
|
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);
|
||||||
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId);
|
|
||||||
Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,
|
|
||||||
Guid confirmingUserId);
|
|
||||||
Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId);
|
Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId);
|
||||||
Task ImportAsync(Guid organizationId, IEnumerable<ImportedGroup> groups,
|
Task ImportAsync(Guid organizationId, IEnumerable<ImportedGroup> groups,
|
||||||
IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<string> removeUserExternalIds,
|
IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<string> removeUserExternalIds,
|
||||||
|
@ -6,6 +6,8 @@ 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.Models.Data.Organizations.Policies;
|
||||||
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.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
@ -76,6 +78,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
private readonly IOrganizationBillingService _organizationBillingService;
|
private readonly IOrganizationBillingService _organizationBillingService;
|
||||||
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
|
|
||||||
public OrganizationService(
|
public OrganizationService(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@ -111,7 +114,8 @@ public class OrganizationService : IOrganizationService
|
|||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IOrganizationBillingService organizationBillingService,
|
IOrganizationBillingService organizationBillingService,
|
||||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
||||||
IPricingClient pricingClient)
|
IPricingClient pricingClient,
|
||||||
|
IPolicyRequirementQuery policyRequirementQuery)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@ -147,6 +151,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
_organizationBillingService = organizationBillingService;
|
_organizationBillingService = organizationBillingService;
|
||||||
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
|
_policyRequirementQuery = policyRequirementQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
|
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
|
||||||
@ -1122,98 +1127,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
|
||||||
Guid confirmingUserId)
|
|
||||||
{
|
|
||||||
var result = await ConfirmUsersAsync(
|
|
||||||
organizationId,
|
|
||||||
new Dictionary<Guid, string>() { { organizationUserId, key } },
|
|
||||||
confirmingUserId);
|
|
||||||
|
|
||||||
if (!result.Any())
|
|
||||||
{
|
|
||||||
throw new BadRequestException("User not valid.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var (orgUser, error) = result[0];
|
|
||||||
if (error != "")
|
|
||||||
{
|
|
||||||
throw new BadRequestException(error);
|
|
||||||
}
|
|
||||||
return orgUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,
|
|
||||||
Guid confirmingUserId)
|
|
||||||
{
|
|
||||||
var selectedOrganizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys);
|
|
||||||
var validSelectedOrganizationUsers = selectedOrganizationUsers
|
|
||||||
.Where(u => u.Status == OrganizationUserStatusType.Accepted && u.OrganizationId == organizationId && u.UserId != null)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (!validSelectedOrganizationUsers.Any())
|
|
||||||
{
|
|
||||||
return new List<Tuple<OrganizationUser, string>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var validSelectedUserIds = validSelectedOrganizationUsers.Select(u => u.UserId.Value).ToList();
|
|
||||||
|
|
||||||
var organization = await GetOrgById(organizationId);
|
|
||||||
var allUsersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validSelectedUserIds);
|
|
||||||
var users = await _userRepository.GetManyAsync(validSelectedUserIds);
|
|
||||||
var usersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(validSelectedUserIds);
|
|
||||||
|
|
||||||
var keyedFilteredUsers = validSelectedOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u);
|
|
||||||
var keyedOrganizationUsers = allUsersOrgs.GroupBy(u => u.UserId.Value)
|
|
||||||
.ToDictionary(u => u.Key, u => u.ToList());
|
|
||||||
|
|
||||||
var succeededUsers = new List<OrganizationUser>();
|
|
||||||
var result = new List<Tuple<OrganizationUser, string>>();
|
|
||||||
|
|
||||||
foreach (var user in users)
|
|
||||||
{
|
|
||||||
if (!keyedFilteredUsers.ContainsKey(user.Id))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
var orgUser = keyedFilteredUsers[user.Id];
|
|
||||||
var orgUsers = keyedOrganizationUsers.GetValueOrDefault(user.Id, new List<OrganizationUser>());
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (organization.PlanType == PlanType.Free && (orgUser.Type == OrganizationUserType.Admin
|
|
||||||
|| orgUser.Type == OrganizationUserType.Owner))
|
|
||||||
{
|
|
||||||
// Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this.
|
|
||||||
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(user.Id);
|
|
||||||
if (adminCount > 0)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("User can only be an admin of one free organization.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var twoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;
|
|
||||||
await CheckPoliciesAsync(organizationId, user, orgUsers, twoFactorEnabled);
|
|
||||||
orgUser.Status = OrganizationUserStatusType.Confirmed;
|
|
||||||
orgUser.Key = keys[orgUser.Id];
|
|
||||||
orgUser.Email = null;
|
|
||||||
|
|
||||||
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
|
||||||
await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, orgUser.AccessSecretsManager);
|
|
||||||
await DeleteAndPushUserRegistrationAsync(organizationId, user.Id);
|
|
||||||
succeededUsers.Add(orgUser);
|
|
||||||
result.Add(Tuple.Create(orgUser, ""));
|
|
||||||
}
|
|
||||||
catch (BadRequestException e)
|
|
||||||
{
|
|
||||||
result.Add(Tuple.Create(orgUser, e.Message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _organizationUserRepository.ReplaceManyAsync(succeededUsers);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal async Task<(bool canScale, string failureReason)> CanScaleAsync(
|
internal async Task<(bool canScale, string failureReason)> CanScaleAsync(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
int seatsToAdd)
|
int seatsToAdd)
|
||||||
@ -1300,32 +1213,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CheckPoliciesAsync(Guid organizationId, User user,
|
|
||||||
ICollection<OrganizationUser> userOrgs, bool twoFactorEnabled)
|
|
||||||
{
|
|
||||||
// Enforce Two Factor Authentication Policy for this organization
|
|
||||||
var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication))
|
|
||||||
.Any(p => p.OrganizationId == organizationId);
|
|
||||||
if (orgRequiresTwoFactor && !twoFactorEnabled)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("User does not have two-step login enabled.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId);
|
|
||||||
var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);
|
|
||||||
var otherSingleOrgPolicies =
|
|
||||||
singleOrgPolicies.Where(p => p.OrganizationId != organizationId);
|
|
||||||
// Enforce Single Organization Policy for this organization
|
|
||||||
if (hasOtherOrgs && singleOrgPolicies.Any(p => p.OrganizationId == organizationId))
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Cannot confirm this member to the organization until they leave or remove all other organizations.");
|
|
||||||
}
|
|
||||||
// Enforce Single Organization Policy of other organizations user is a member of
|
|
||||||
if (otherSingleOrgPolicies.Any())
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Cannot confirm this member to the organization because they are in another organization which forbids it.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId)
|
public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId)
|
||||||
{
|
{
|
||||||
@ -1353,13 +1241,25 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Block the user from withdrawal if auto enrollment is enabled
|
// Block the user from withdrawal if auto enrollment is enabled
|
||||||
if (resetPasswordKey == null && resetPasswordPolicy.Data != null)
|
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
|
||||||
{
|
{
|
||||||
var data = JsonSerializer.Deserialize<ResetPasswordDataModel>(resetPasswordPolicy.Data, JsonHelpers.IgnoreCase);
|
var resetPasswordPolicyRequirement = await _policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(userId);
|
||||||
|
if (resetPasswordKey == null && resetPasswordPolicyRequirement.AutoEnrollEnabled(organizationId))
|
||||||
if (data?.AutoEnrollEnabled ?? false)
|
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to withdraw from Password Reset.");
|
throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to withdraw from account recovery.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (resetPasswordKey == null && resetPasswordPolicy.Data != null)
|
||||||
|
{
|
||||||
|
var data = JsonSerializer.Deserialize<ResetPasswordDataModel>(resetPasswordPolicy.Data, JsonHelpers.IgnoreCase);
|
||||||
|
|
||||||
|
if (data?.AutoEnrollEnabled ?? false)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to withdraw from account recovery.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1623,15 +1523,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
await _groupRepository.UpdateUsersAsync(group.Id, users);
|
await _groupRepository.UpdateUsersAsync(group.Id, users);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
|
|
||||||
{
|
|
||||||
var devices = await GetUserDeviceIdsAsync(userId);
|
|
||||||
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices,
|
|
||||||
organizationId.ToString());
|
|
||||||
await _pushNotificationService.PushSyncOrgKeysAsync(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
|
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
|
||||||
{
|
{
|
||||||
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
||||||
|
@ -118,6 +118,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string ExportAttachments = "export-attachments";
|
public const string ExportAttachments = "export-attachments";
|
||||||
|
|
||||||
/* Vault Team */
|
/* Vault Team */
|
||||||
|
public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge";
|
||||||
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
|
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
|
||||||
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
|
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
|
||||||
public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
|
public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
|
||||||
@ -125,6 +126,9 @@ public static class FeatureFlagKeys
|
|||||||
public const string RestrictProviderAccess = "restrict-provider-access";
|
public const string RestrictProviderAccess = "restrict-provider-access";
|
||||||
public const string SecurityTasks = "security-tasks";
|
public const string SecurityTasks = "security-tasks";
|
||||||
|
|
||||||
|
/* Auth Team */
|
||||||
|
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
|
||||||
|
|
||||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||||
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
||||||
public const string DuoRedirect = "duo-redirect";
|
public const string DuoRedirect = "duo-redirect";
|
||||||
@ -177,6 +181,8 @@ public static class FeatureFlagKeys
|
|||||||
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";
|
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";
|
||||||
public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
|
public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
|
||||||
public const string PM3553_MobileSimpleLoginSelfHostAlias = "simple-login-self-host-alias";
|
public const string PM3553_MobileSimpleLoginSelfHostAlias = "simple-login-self-host-alias";
|
||||||
|
public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor";
|
||||||
|
public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor";
|
||||||
|
|
||||||
public static List<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
{
|
{
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<GenerateUserSecretsAttribute>false</GenerateUserSecretsAttribute>
|
<GenerateUserSecretsAttribute>false</GenerateUserSecretsAttribute>
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
<!-- Temp exclusions until warnings are fixed -->
|
<!-- Temp exclusions until warnings are fixed -->
|
||||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS1570;CS1574;CS8602;CS9113;CS1998;CS8604</WarningsNotAsErrors>
|
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS1570;CS1574;CS9113;CS1998</WarningsNotAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
@ -15,14 +15,21 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<table width="100%" border="0" cellpadding="0" cellspacing="0"
|
<table width="100%" border="0" cellpadding="0" cellspacing="0"
|
||||||
style="display: table; width:100%; padding-bottom: 35px; text-align: center;" align="center">
|
style="display: table; width:100%; padding-bottom: 24px; text-align: center;" align="center">
|
||||||
<tr>
|
<tr>
|
||||||
<td display="display: table-cell">
|
<td display="display: table-cell">
|
||||||
<a href="{{ReviewPasswordsUrl}}" clicktracking=off target="_blank"
|
<a href="{{ReviewPasswordsUrl}}" clicktracking=off target="_blank"
|
||||||
style="display: inline-block; color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; border-radius: 999px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
style="display: inline-block; font-weight: bold; color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; border-radius: 999px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
Review at-risk passwords
|
Review at-risk passwords
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<table width="100%" border="0" cellpadding="0" cellspacing="0"
|
||||||
|
style="display: table; width:100%; padding-bottom: 24px; text-align: center;" align="center">
|
||||||
|
<tr>
|
||||||
|
<td display="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 12px; line-height: 16px;">
|
||||||
|
{{formatAdminOwnerEmails AdminOwnerEmails}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
{{/SecurityTasksHtmlLayout}}
|
{{/SecurityTasksHtmlLayout}}
|
||||||
|
@ -5,4 +5,13 @@ breach.
|
|||||||
Launch the Bitwarden extension to review your at-risk passwords.
|
Launch the Bitwarden extension to review your at-risk passwords.
|
||||||
|
|
||||||
Review at-risk passwords ({{{ReviewPasswordsUrl}}})
|
Review at-risk passwords ({{{ReviewPasswordsUrl}}})
|
||||||
|
|
||||||
|
{{#if (eq (length AdminOwnerEmails) 1)}}
|
||||||
|
This request was initiated by {{AdminOwnerEmails.[0]}}.
|
||||||
|
{{else}}
|
||||||
|
This request was initiated by
|
||||||
|
{{#each AdminOwnerEmails}}
|
||||||
|
{{#if @last}}and {{/if}}{{this}}{{#unless @last}}, {{/unless}}
|
||||||
|
{{/each}}.
|
||||||
|
{{/if}}
|
||||||
{{/SecurityTasksHtmlLayout}}
|
{{/SecurityTasksHtmlLayout}}
|
||||||
|
@ -8,5 +8,7 @@ public class SecurityTaskNotificationViewModel : BaseMailModel
|
|||||||
|
|
||||||
public bool TaskCountPlural => TaskCount != 1;
|
public bool TaskCountPlural => TaskCount != 1;
|
||||||
|
|
||||||
|
public IEnumerable<string> AdminOwnerEmails { get; set; }
|
||||||
|
|
||||||
public string ReviewPasswordsUrl => $"{WebVaultUrl}/browser-extension-prompt";
|
public string ReviewPasswordsUrl => $"{WebVaultUrl}/browser-extension-prompt";
|
||||||
}
|
}
|
||||||
|
@ -116,6 +116,7 @@ public static class OrganizationServiceCollectionExtensions
|
|||||||
services.AddScoped<IUpdateOrganizationUserCommand, UpdateOrganizationUserCommand>();
|
services.AddScoped<IUpdateOrganizationUserCommand, UpdateOrganizationUserCommand>();
|
||||||
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
|
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
|
||||||
services.AddScoped<IDeleteManagedOrganizationUserAccountCommand, DeleteManagedOrganizationUserAccountCommand>();
|
services.AddScoped<IDeleteManagedOrganizationUserAccountCommand, DeleteManagedOrganizationUserAccountCommand>();
|
||||||
|
services.AddScoped<IConfirmOrganizationUserCommand, ConfirmOrganizationUserCommand>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services)
|
private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services)
|
||||||
|
@ -99,5 +99,5 @@ public interface IMailService
|
|||||||
string organizationName);
|
string organizationName);
|
||||||
Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList);
|
Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList);
|
||||||
Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable<string> adminEmails, Guid organizationId, string email, string userName);
|
Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable<string> adminEmails, Guid organizationId, string email, string userName);
|
||||||
Task SendBulkSecurityTaskNotificationsAsync(string orgName, IEnumerable<UserSecurityTasksCount> securityTaskNotificaitons);
|
Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications, IEnumerable<string> adminOwnerEmails);
|
||||||
}
|
}
|
||||||
|
@ -740,6 +740,45 @@ public class HandlebarsMailService : IMailService
|
|||||||
var clickTrackingText = (clickTrackingOff ? "clicktracking=off" : string.Empty);
|
var clickTrackingText = (clickTrackingOff ? "clicktracking=off" : string.Empty);
|
||||||
writer.WriteSafeString($"<a href=\"{href}\" target=\"_blank\" {clickTrackingText}>{text}</a>");
|
writer.WriteSafeString($"<a href=\"{href}\" target=\"_blank\" {clickTrackingText}>{text}</a>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Construct markup for admin and owner email addresses.
|
||||||
|
// Using conditionals within the handlebar syntax was including extra spaces around
|
||||||
|
// concatenated strings, which this helper avoids.
|
||||||
|
Handlebars.RegisterHelper("formatAdminOwnerEmails", (writer, context, parameters) =>
|
||||||
|
{
|
||||||
|
if (parameters.Length == 0)
|
||||||
|
{
|
||||||
|
writer.WriteSafeString(string.Empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var emailList = ((IEnumerable<string>)parameters[0]).ToList();
|
||||||
|
if (emailList.Count == 0)
|
||||||
|
{
|
||||||
|
writer.WriteSafeString(string.Empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string constructAnchorElement(string email)
|
||||||
|
{
|
||||||
|
return $"<a style=\"color: #175DDC\" href=\"mailto:{email}\">{email}</a>";
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputMessage = "This request was initiated by ";
|
||||||
|
|
||||||
|
if (emailList.Count == 1)
|
||||||
|
{
|
||||||
|
outputMessage += $"{constructAnchorElement(emailList[0])}.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outputMessage += string.Join(", ", emailList.Take(emailList.Count - 1)
|
||||||
|
.Select(email => constructAnchorElement(email)));
|
||||||
|
outputMessage += $", and {constructAnchorElement(emailList.Last())}.";
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteSafeString($"{outputMessage}");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token)
|
public async Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token)
|
||||||
@ -1201,21 +1240,23 @@ public class HandlebarsMailService : IMailService
|
|||||||
await _mailDeliveryService.SendEmailAsync(message);
|
await _mailDeliveryService.SendEmailAsync(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendBulkSecurityTaskNotificationsAsync(string orgName, IEnumerable<UserSecurityTasksCount> securityTaskNotificaitons)
|
public async Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications, IEnumerable<string> adminOwnerEmails)
|
||||||
{
|
{
|
||||||
MailQueueMessage CreateMessage(UserSecurityTasksCount notification)
|
MailQueueMessage CreateMessage(UserSecurityTasksCount notification)
|
||||||
{
|
{
|
||||||
var message = CreateDefaultMessage($"{orgName} has identified {notification.TaskCount} at-risk password{(notification.TaskCount.Equals(1) ? "" : "s")}", notification.Email);
|
var sanitizedOrgName = CoreHelpers.SanitizeForEmail(org.DisplayName(), false);
|
||||||
|
var message = CreateDefaultMessage($"{sanitizedOrgName} has identified {notification.TaskCount} at-risk password{(notification.TaskCount.Equals(1) ? "" : "s")}", notification.Email);
|
||||||
var model = new SecurityTaskNotificationViewModel
|
var model = new SecurityTaskNotificationViewModel
|
||||||
{
|
{
|
||||||
OrgName = orgName,
|
OrgName = CoreHelpers.SanitizeForEmail(sanitizedOrgName, false),
|
||||||
TaskCount = notification.TaskCount,
|
TaskCount = notification.TaskCount,
|
||||||
|
AdminOwnerEmails = adminOwnerEmails,
|
||||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||||
};
|
};
|
||||||
message.Category = "SecurityTasksNotification";
|
message.Category = "SecurityTasksNotification";
|
||||||
return new MailQueueMessage(message, "SecurityTasksNotification", model);
|
return new MailQueueMessage(message, "SecurityTasksNotification", model);
|
||||||
}
|
}
|
||||||
var messageModels = securityTaskNotificaitons.Select(CreateMessage);
|
var messageModels = securityTaskNotifications.Select(CreateMessage);
|
||||||
await EnqueueMailAsync(messageModels.ToList());
|
await EnqueueMailAsync(messageModels.ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -324,7 +324,7 @@ public class NoopMailService : IMailService
|
|||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SendBulkSecurityTaskNotificationsAsync(string orgName, IEnumerable<UserSecurityTasksCount> securityTaskNotificaitons)
|
public Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications, IEnumerable<string> adminOwnerEmails)
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
@ -17,19 +17,22 @@ public class CreateManyTaskNotificationsCommand : ICreateManyTaskNotificationsCo
|
|||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
private readonly ICreateNotificationCommand _createNotificationCommand;
|
private readonly ICreateNotificationCommand _createNotificationCommand;
|
||||||
private readonly IPushNotificationService _pushNotificationService;
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
|
||||||
public CreateManyTaskNotificationsCommand(
|
public CreateManyTaskNotificationsCommand(
|
||||||
IGetSecurityTasksNotificationDetailsQuery getSecurityTasksNotificationDetailsQuery,
|
IGetSecurityTasksNotificationDetailsQuery getSecurityTasksNotificationDetailsQuery,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
ICreateNotificationCommand createNotificationCommand,
|
ICreateNotificationCommand createNotificationCommand,
|
||||||
IPushNotificationService pushNotificationService)
|
IPushNotificationService pushNotificationService,
|
||||||
|
IOrganizationUserRepository organizationUserRepository)
|
||||||
{
|
{
|
||||||
_getSecurityTasksNotificationDetailsQuery = getSecurityTasksNotificationDetailsQuery;
|
_getSecurityTasksNotificationDetailsQuery = getSecurityTasksNotificationDetailsQuery;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
_createNotificationCommand = createNotificationCommand;
|
_createNotificationCommand = createNotificationCommand;
|
||||||
_pushNotificationService = pushNotificationService;
|
_pushNotificationService = pushNotificationService;
|
||||||
|
_organizationUserRepository = organizationUserRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateAsync(Guid orgId, IEnumerable<SecurityTask> securityTasks)
|
public async Task CreateAsync(Guid orgId, IEnumerable<SecurityTask> securityTasks)
|
||||||
@ -45,8 +48,11 @@ public class CreateManyTaskNotificationsCommand : ICreateManyTaskNotificationsCo
|
|||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(orgId);
|
var organization = await _organizationRepository.GetByIdAsync(orgId);
|
||||||
|
var orgAdminEmails = await _organizationUserRepository.GetManyDetailsByRoleAsync(orgId, OrganizationUserType.Admin);
|
||||||
|
var orgOwnerEmails = await _organizationUserRepository.GetManyDetailsByRoleAsync(orgId, OrganizationUserType.Owner);
|
||||||
|
var orgAdminAndOwnerEmails = orgAdminEmails.Concat(orgOwnerEmails).Select(x => x.Email).Distinct().ToList();
|
||||||
|
|
||||||
await _mailService.SendBulkSecurityTaskNotificationsAsync(organization.Name, userTaskCount);
|
await _mailService.SendBulkSecurityTaskNotificationsAsync(organization, userTaskCount, orgAdminAndOwnerEmails);
|
||||||
|
|
||||||
// Break securityTaskCiphers into separate lists by user Id
|
// Break securityTaskCiphers into separate lists by user Id
|
||||||
var securityTaskCiphersByUser = securityTaskCiphers.GroupBy(x => x.UserId)
|
var securityTaskCiphersByUser = securityTaskCiphers.GroupBy(x => x.UserId)
|
||||||
|
@ -13,7 +13,9 @@ using Bit.Core.Tools.Models.Business;
|
|||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
|
using Bit.Core.Vault.Enums;
|
||||||
using Bit.Core.Vault.Models.Data;
|
using Bit.Core.Vault.Models.Data;
|
||||||
|
using Bit.Core.Vault.Queries;
|
||||||
using Bit.Core.Vault.Repositories;
|
using Bit.Core.Vault.Repositories;
|
||||||
|
|
||||||
namespace Bit.Core.Vault.Services;
|
namespace Bit.Core.Vault.Services;
|
||||||
@ -38,6 +40,7 @@ public class CipherService : ICipherService
|
|||||||
private const long _fileSizeLeeway = 1024L * 1024L; // 1MB
|
private const long _fileSizeLeeway = 1024L * 1024L; // 1MB
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
|
private readonly IGetCipherPermissionsForUserQuery _getCipherPermissionsForUserQuery;
|
||||||
|
|
||||||
public CipherService(
|
public CipherService(
|
||||||
ICipherRepository cipherRepository,
|
ICipherRepository cipherRepository,
|
||||||
@ -54,7 +57,8 @@ public class CipherService : ICipherService
|
|||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IReferenceEventService referenceEventService,
|
IReferenceEventService referenceEventService,
|
||||||
ICurrentContext currentContext)
|
ICurrentContext currentContext,
|
||||||
|
IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery)
|
||||||
{
|
{
|
||||||
_cipherRepository = cipherRepository;
|
_cipherRepository = cipherRepository;
|
||||||
_folderRepository = folderRepository;
|
_folderRepository = folderRepository;
|
||||||
@ -71,6 +75,7 @@ public class CipherService : ICipherService
|
|||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_referenceEventService = referenceEventService;
|
_referenceEventService = referenceEventService;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
|
_getCipherPermissionsForUserQuery = getCipherPermissionsForUserQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
|
public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
|
||||||
@ -161,6 +166,7 @@ public class CipherService : ICipherService
|
|||||||
{
|
{
|
||||||
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
|
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
|
||||||
cipher.RevisionDate = DateTime.UtcNow;
|
cipher.RevisionDate = DateTime.UtcNow;
|
||||||
|
await ValidateViewPasswordUserAsync(cipher);
|
||||||
await _cipherRepository.ReplaceAsync(cipher);
|
await _cipherRepository.ReplaceAsync(cipher);
|
||||||
await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_Updated);
|
await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_Updated);
|
||||||
|
|
||||||
@ -966,4 +972,32 @@ public class CipherService : ICipherService
|
|||||||
|
|
||||||
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
|
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ValidateViewPasswordUserAsync(Cipher cipher)
|
||||||
|
{
|
||||||
|
if (cipher.Type != CipherType.Login || cipher.Data == null || !cipher.OrganizationId.HasValue)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var existingCipher = await _cipherRepository.GetByIdAsync(cipher.Id);
|
||||||
|
if (existingCipher == null) return;
|
||||||
|
|
||||||
|
var cipherPermissions = await _getCipherPermissionsForUserQuery.GetByOrganization(cipher.OrganizationId.Value);
|
||||||
|
// Check if user is a "hidden password" user
|
||||||
|
if (!cipherPermissions.TryGetValue(cipher.Id, out var permission) || !(permission.ViewPassword && permission.Edit))
|
||||||
|
{
|
||||||
|
// "hidden password" users may not add cipher key encryption
|
||||||
|
if (existingCipher.Key == null && cipher.Key != null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You do not have permission to add cipher key encryption.");
|
||||||
|
}
|
||||||
|
// "hidden password" users may not change passwords, TOTP codes, or passkeys, so we need to set them back to the original values
|
||||||
|
var existingCipherData = JsonSerializer.Deserialize<CipherLoginData>(existingCipher.Data);
|
||||||
|
var newCipherData = JsonSerializer.Deserialize<CipherLoginData>(cipher.Data);
|
||||||
|
newCipherData.Fido2Credentials = existingCipherData.Fido2Credentials;
|
||||||
|
newCipherData.Totp = existingCipherData.Totp;
|
||||||
|
newCipherData.Password = existingCipherData.Password;
|
||||||
|
cipher.Data = JsonSerializer.Serialize(newCipherData);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Temp exclusions until warnings are fixed -->
|
<!-- Temp exclusions until warnings are fixed -->
|
||||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8618;CS4014</WarningsNotAsErrors>
|
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8618</WarningsNotAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -7,6 +7,8 @@ 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.Models.Data.Organizations.Policies;
|
||||||
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.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
@ -424,4 +426,93 @@ public class OrganizationUsersControllerTests
|
|||||||
.GetManyDetailsByOrganizationAsync(organizationAbility.Id, Arg.Any<bool>(), Arg.Any<bool>())
|
.GetManyDetailsByOrganizationAsync(organizationAbility.Id, Arg.Any<bool>(), Arg.Any<bool>())
|
||||||
.Returns(organizationUsers);
|
.Returns(organizationUsers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task Accept_WhenOrganizationUsePoliciesIsEnabledAndResetPolicyIsEnabled_WithPolicyRequirementsEnabled_ShouldHandleResetPassword(Guid orgId, Guid orgUserId,
|
||||||
|
OrganizationUserAcceptRequestModel model, User user, SutProvider<OrganizationUsersController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
|
||||||
|
applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = true });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var policy = new Policy
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }),
|
||||||
|
};
|
||||||
|
var userService = sutProvider.GetDependency<IUserService>();
|
||||||
|
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||||
|
|
||||||
|
var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
|
||||||
|
|
||||||
|
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||||
|
|
||||||
|
var policyRequirement = new ResetPasswordPolicyRequirement { AutoEnrollOrganizations = [orgId] };
|
||||||
|
|
||||||
|
policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id).Returns(policyRequirement);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.Accept(orgId, orgUserId, model);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1)
|
||||||
|
.AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService);
|
||||||
|
await sutProvider.GetDependency<IOrganizationService>().Received(1)
|
||||||
|
.UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);
|
||||||
|
|
||||||
|
await userService.Received(1).GetUserByPrincipalAsync(default);
|
||||||
|
await applicationCacheService.Received(0).GetOrganizationAbilityAsync(orgId);
|
||||||
|
await policyRepository.Received(0).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
||||||
|
await policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||||
|
Assert.True(policyRequirement.AutoEnrollEnabled(orgId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task Accept_WithInvalidModelResetPasswordKey_WithPolicyRequirementsEnabled_ThrowsBadRequestException(Guid orgId, Guid orgUserId,
|
||||||
|
OrganizationUserAcceptRequestModel model, User user, SutProvider<OrganizationUsersController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
model.ResetPasswordKey = " ";
|
||||||
|
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
|
||||||
|
applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = true });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var policy = new Policy
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }),
|
||||||
|
};
|
||||||
|
var userService = sutProvider.GetDependency<IUserService>();
|
||||||
|
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||||
|
|
||||||
|
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||||
|
|
||||||
|
var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
|
||||||
|
|
||||||
|
var policyRequirement = new ResetPasswordPolicyRequirement { AutoEnrollOrganizations = [orgId] };
|
||||||
|
|
||||||
|
policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id).Returns(policyRequirement);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
|
sutProvider.Sut.Accept(orgId, orgUserId, model));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(0)
|
||||||
|
.AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService);
|
||||||
|
await sutProvider.GetDependency<IOrganizationService>().Received(0)
|
||||||
|
.UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);
|
||||||
|
|
||||||
|
await userService.Received(1).GetUserByPrincipalAsync(default);
|
||||||
|
await applicationCacheService.Received(0).GetOrganizationAbilityAsync(orgId);
|
||||||
|
await policyRepository.Received(0).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
||||||
|
await policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||||
|
|
||||||
|
Assert.Equal("Master Password reset is required, but not provided.", exception.Message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,15 @@ using Bit.Api.AdminConsole.Controllers;
|
|||||||
using Bit.Api.Auth.Models.Request.Accounts;
|
using Bit.Api.Auth.Models.Request.Accounts;
|
||||||
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.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||||
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.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
@ -55,6 +58,7 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||||
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
|
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
|
||||||
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
|
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
|
||||||
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
private readonly OrganizationsController _sut;
|
private readonly OrganizationsController _sut;
|
||||||
|
|
||||||
@ -80,6 +84,7 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
_removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>();
|
_removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>();
|
||||||
_cloudOrganizationSignUpCommand = Substitute.For<ICloudOrganizationSignUpCommand>();
|
_cloudOrganizationSignUpCommand = Substitute.For<ICloudOrganizationSignUpCommand>();
|
||||||
_organizationDeleteCommand = Substitute.For<IOrganizationDeleteCommand>();
|
_organizationDeleteCommand = Substitute.For<IOrganizationDeleteCommand>();
|
||||||
|
_policyRequirementQuery = Substitute.For<IPolicyRequirementQuery>();
|
||||||
_pricingClient = Substitute.For<IPricingClient>();
|
_pricingClient = Substitute.For<IPricingClient>();
|
||||||
|
|
||||||
_sut = new OrganizationsController(
|
_sut = new OrganizationsController(
|
||||||
@ -103,6 +108,7 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
_removeOrganizationUserCommand,
|
_removeOrganizationUserCommand,
|
||||||
_cloudOrganizationSignUpCommand,
|
_cloudOrganizationSignUpCommand,
|
||||||
_organizationDeleteCommand,
|
_organizationDeleteCommand,
|
||||||
|
_policyRequirementQuery,
|
||||||
_pricingClient);
|
_pricingClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,4 +242,55 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
|
|
||||||
await _organizationDeleteCommand.Received(1).DeleteAsync(organization);
|
await _organizationDeleteCommand.Received(1).DeleteAsync(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task GetAutoEnrollStatus_WithPolicyRequirementsEnabled_ReturnsOrganizationAutoEnrollStatus_WithResetPasswordEnabledTrue(
|
||||||
|
User user,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser organizationUser
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var policyRequirement = new ResetPasswordPolicyRequirement() { AutoEnrollOrganizations = [organization.Id] };
|
||||||
|
|
||||||
|
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||||
|
_organizationRepository.GetByIdentifierAsync(organization.Id.ToString()).Returns(organization);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
_organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id).Returns(organizationUser);
|
||||||
|
_policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id).Returns(policyRequirement);
|
||||||
|
|
||||||
|
var result = await _sut.GetAutoEnrollStatus(organization.Id.ToString());
|
||||||
|
|
||||||
|
await _userService.Received(1).GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>());
|
||||||
|
await _organizationRepository.Received(1).GetByIdentifierAsync(organization.Id.ToString());
|
||||||
|
await _policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||||
|
|
||||||
|
Assert.True(result.ResetPasswordEnabled);
|
||||||
|
Assert.Equal(result.Id, organization.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task GetAutoEnrollStatus_WithPolicyRequirementsDisabled_ReturnsOrganizationAutoEnrollStatus_WithResetPasswordEnabledTrue(
|
||||||
|
User user,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser organizationUser
|
||||||
|
)
|
||||||
|
{
|
||||||
|
|
||||||
|
var policy = new Policy() { Type = PolicyType.ResetPassword, Enabled = true, Data = "{\"AutoEnrollEnabled\": true}", OrganizationId = organization.Id };
|
||||||
|
|
||||||
|
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||||
|
_organizationRepository.GetByIdentifierAsync(organization.Id.ToString()).Returns(organization);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(false);
|
||||||
|
_organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id).Returns(organizationUser);
|
||||||
|
_policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword).Returns(policy);
|
||||||
|
|
||||||
|
var result = await _sut.GetAutoEnrollStatus(organization.Id.ToString());
|
||||||
|
|
||||||
|
await _userService.Received(1).GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>());
|
||||||
|
await _organizationRepository.Received(1).GetByIdentifierAsync(organization.Id.ToString());
|
||||||
|
await _policyRequirementQuery.Received(0).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||||
|
await _policyRepository.Received(1).GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
|
||||||
|
|
||||||
|
Assert.True(result.ResetPasswordEnabled);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,324 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||||
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||||
|
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class ConfirmOrganizationUserCommandTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ConfirmUserAsync_WithInvalidStatus_ThrowsBadRequestException(OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser orgUser, string key,
|
||||||
|
SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||||
|
Assert.Contains("User not valid.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ConfirmUserAsync_WithWrongOrganization_ThrowsBadRequestException(OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, string key,
|
||||||
|
SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.ConfirmUserAsync(confirmingUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||||
|
Assert.Contains("User not valid.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
|
public async Task ConfirmUserAsync_ToFree_WithExistingAdminOrOwner_ThrowsBadRequestException(OrganizationUserType userType, Organization org, OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||||
|
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
|
||||||
|
org.PlanType = PlanType.Free;
|
||||||
|
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||||
|
orgUser.UserId = user.Id;
|
||||||
|
orgUser.Type = userType;
|
||||||
|
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||||
|
organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(orgUser.UserId.Value).Returns(1);
|
||||||
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
|
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||||
|
Assert.Contains("User can only be an admin of one free organization.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.Custom, OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(PlanType.Custom, OrganizationUserType.Owner)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually, OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually, OrganizationUserType.Owner)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2020, OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2020, OrganizationUserType.Owner)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2019, OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2019, OrganizationUserType.Owner)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly, OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly, OrganizationUserType.Owner)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2020, OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2020, OrganizationUserType.Owner)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2019, OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2019, OrganizationUserType.Owner)]
|
||||||
|
[BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Owner)]
|
||||||
|
[BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Owner)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually, OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually, OrganizationUserType.Owner)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2020, OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2020, OrganizationUserType.Owner)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2019, OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2019, OrganizationUserType.Owner)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly, OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly, OrganizationUserType.Owner)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2020, OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2020, OrganizationUserType.Owner)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2019, OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2019, OrganizationUserType.Owner)]
|
||||||
|
public async Task ConfirmUserAsync_ToNonFree_WithExistingFreeAdminOrOwner_Succeeds(PlanType planType, OrganizationUserType orgUserType, Organization org, OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||||
|
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
|
||||||
|
org.PlanType = planType;
|
||||||
|
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||||
|
orgUser.UserId = user.Id;
|
||||||
|
orgUser.Type = orgUserType;
|
||||||
|
orgUser.AccessSecretsManager = false;
|
||||||
|
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||||
|
organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(orgUser.UserId.Value).Returns(1);
|
||||||
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
|
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||||
|
|
||||||
|
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||||
|
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email);
|
||||||
|
await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ConfirmUserAsync_AsUser_WithSingleOrgPolicyAppliedFromConfirmingOrg_ThrowsBadRequestException(Organization org, OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||||
|
OrganizationUser orgUserAnotherOrg, [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,
|
||||||
|
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||||
|
|
||||||
|
org.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
orgUser.Status = OrganizationUserStatusType.Accepted;
|
||||||
|
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||||
|
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
||||||
|
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||||
|
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
||||||
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
|
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||||
|
singleOrgPolicy.OrganizationId = org.Id;
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy });
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||||
|
Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ConfirmUserAsync_AsUser_WithSingleOrgPolicyAppliedFromOtherOrg_ThrowsBadRequestException(Organization org, OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||||
|
OrganizationUser orgUserAnotherOrg, [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,
|
||||||
|
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||||
|
|
||||||
|
org.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
orgUser.Status = OrganizationUserStatusType.Accepted;
|
||||||
|
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||||
|
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
||||||
|
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||||
|
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
||||||
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
|
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||||
|
singleOrgPolicy.OrganizationId = orgUserAnotherOrg.Id;
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy });
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||||
|
Assert.Contains("Cannot confirm this member to the organization because they are in another organization which forbids it.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
|
public async Task ConfirmUserAsync_AsOwnerOrAdmin_WithSingleOrgPolicy_ExcludedViaUserType_Success(
|
||||||
|
OrganizationUserType userType, Organization org, OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||||
|
OrganizationUser orgUserAnotherOrg,
|
||||||
|
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
|
||||||
|
org.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
orgUser.Type = userType;
|
||||||
|
orgUser.Status = OrganizationUserStatusType.Accepted;
|
||||||
|
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||||
|
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
||||||
|
orgUser.AccessSecretsManager = true;
|
||||||
|
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||||
|
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
||||||
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
|
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||||
|
|
||||||
|
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||||
|
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, true);
|
||||||
|
await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ConfirmUserAsync_WithTwoFactorPolicyAndTwoFactorDisabled_ThrowsBadRequestException(Organization org, OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||||
|
OrganizationUser orgUserAnotherOrg,
|
||||||
|
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
||||||
|
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||||
|
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||||
|
|
||||||
|
org.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||||
|
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
||||||
|
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||||
|
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
||||||
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
|
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||||
|
twoFactorPolicy.OrganizationId = org.Id;
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
||||||
|
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
|
||||||
|
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||||
|
Assert.Contains("User does not have two-step login enabled.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ConfirmUserAsync_WithTwoFactorPolicyAndTwoFactorEnabled_Succeeds(Organization org, OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||||
|
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
||||||
|
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||||
|
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||||
|
|
||||||
|
org.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||||
|
orgUser.UserId = user.Id;
|
||||||
|
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||||
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
|
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||||
|
twoFactorPolicy.OrganizationId = org.Id;
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
||||||
|
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
|
||||||
|
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, true) });
|
||||||
|
|
||||||
|
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ConfirmUsersAsync_WithMultipleUsers_ReturnsExpectedMixedResults(Organization org,
|
||||||
|
OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser1,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser3,
|
||||||
|
OrganizationUser anotherOrgUser, User user1, User user2, User user3,
|
||||||
|
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
||||||
|
[OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,
|
||||||
|
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||||
|
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||||
|
|
||||||
|
org.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||||
|
orgUser1.UserId = user1.Id;
|
||||||
|
orgUser2.UserId = user2.Id;
|
||||||
|
orgUser3.UserId = user3.Id;
|
||||||
|
anotherOrgUser.UserId = user3.Id;
|
||||||
|
var orgUsers = new[] { orgUser1, orgUser2, orgUser3 };
|
||||||
|
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(orgUsers);
|
||||||
|
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||||
|
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user1, user2, user3 });
|
||||||
|
twoFactorPolicy.OrganizationId = org.Id;
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
||||||
|
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id) && ids.Contains(user3.Id)))
|
||||||
|
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>()
|
||||||
|
{
|
||||||
|
(user1.Id, true),
|
||||||
|
(user2.Id, false),
|
||||||
|
(user3.Id, true)
|
||||||
|
});
|
||||||
|
singleOrgPolicy.OrganizationId = org.Id;
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(user3.Id, PolicyType.SingleOrg)
|
||||||
|
.Returns(new[] { singleOrgPolicy });
|
||||||
|
organizationUserRepository.GetManyByManyUsersAsync(default)
|
||||||
|
.ReturnsForAnyArgs(new[] { orgUser1, orgUser2, orgUser3, anotherOrgUser });
|
||||||
|
|
||||||
|
var keys = orgUsers.ToDictionary(ou => ou.Id, _ => key);
|
||||||
|
var result = await sutProvider.Sut.ConfirmUsersAsync(confirmingUser.OrganizationId, keys, confirmingUser.Id);
|
||||||
|
Assert.Contains("", result[0].Item2);
|
||||||
|
Assert.Contains("User does not have two-step login enabled.", result[1].Item2);
|
||||||
|
Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class ResetPasswordPolicyRequirementFactoryTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void AutoEnroll_WithNoPolicies_IsEmpty(SutProvider<ResetPasswordPolicyRequirementFactory> sutProvider, Guid orgId)
|
||||||
|
{
|
||||||
|
var actual = sutProvider.Sut.Create([]);
|
||||||
|
|
||||||
|
Assert.False(actual.AutoEnrollEnabled(orgId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void AutoEnrollAdministration_WithAnyResetPasswordPolices_ReturnsEnabledOrganizationIds(
|
||||||
|
[PolicyDetails(PolicyType.ResetPassword)] PolicyDetails[] policies,
|
||||||
|
SutProvider<ResetPasswordPolicyRequirementFactory> sutProvider)
|
||||||
|
{
|
||||||
|
policies[0].SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true });
|
||||||
|
policies[1].SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = false });
|
||||||
|
policies[2].SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true });
|
||||||
|
|
||||||
|
var actual = sutProvider.Sut.Create(policies);
|
||||||
|
|
||||||
|
Assert.True(actual.AutoEnrollEnabled(policies[0].OrganizationId));
|
||||||
|
Assert.False(actual.AutoEnrollEnabled(policies[1].OrganizationId));
|
||||||
|
Assert.True(actual.AutoEnrollEnabled(policies[2].OrganizationId));
|
||||||
|
}
|
||||||
|
}
|
@ -24,7 +24,6 @@ using Bit.Core.Platform.Push;
|
|||||||
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.Test.AutoFixture.OrganizationFixtures;
|
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
@ -978,306 +977,6 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task ConfirmUser_InvalidStatus(OrganizationUser confirmingUser,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser orgUser, string key,
|
|
||||||
SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
|
||||||
|
|
||||||
organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser);
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
|
||||||
Assert.Contains("User not valid.", exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task ConfirmUser_WrongOrganization(OrganizationUser confirmingUser,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, string key,
|
|
||||||
SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
|
||||||
|
|
||||||
organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser);
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.ConfirmUserAsync(confirmingUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
|
||||||
Assert.Contains("User not valid.", exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData(OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(OrganizationUserType.Owner)]
|
|
||||||
public async Task ConfirmUserToFree_AlreadyFreeAdminOrOwner_Throws(OrganizationUserType userType, Organization org, OrganizationUser confirmingUser,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
|
||||||
string key, SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
|
||||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
|
||||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
|
||||||
|
|
||||||
org.PlanType = PlanType.Free;
|
|
||||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
|
||||||
orgUser.UserId = user.Id;
|
|
||||||
orgUser.Type = userType;
|
|
||||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
|
||||||
organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(orgUser.UserId.Value).Returns(1);
|
|
||||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
|
||||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
|
||||||
Assert.Contains("User can only be an admin of one free organization.", exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData(PlanType.Custom, OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(PlanType.Custom, OrganizationUserType.Owner)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseAnnually, OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseAnnually, OrganizationUserType.Owner)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseAnnually2020, OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseAnnually2020, OrganizationUserType.Owner)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseAnnually2019, OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseAnnually2019, OrganizationUserType.Owner)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseMonthly, OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseMonthly, OrganizationUserType.Owner)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseMonthly2020, OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseMonthly2020, OrganizationUserType.Owner)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseMonthly2019, OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseMonthly2019, OrganizationUserType.Owner)]
|
|
||||||
[BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Owner)]
|
|
||||||
[BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Owner)]
|
|
||||||
[BitAutoData(PlanType.TeamsAnnually, OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(PlanType.TeamsAnnually, OrganizationUserType.Owner)]
|
|
||||||
[BitAutoData(PlanType.TeamsAnnually2020, OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(PlanType.TeamsAnnually2020, OrganizationUserType.Owner)]
|
|
||||||
[BitAutoData(PlanType.TeamsAnnually2019, OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(PlanType.TeamsAnnually2019, OrganizationUserType.Owner)]
|
|
||||||
[BitAutoData(PlanType.TeamsMonthly, OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(PlanType.TeamsMonthly, OrganizationUserType.Owner)]
|
|
||||||
[BitAutoData(PlanType.TeamsMonthly2020, OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(PlanType.TeamsMonthly2020, OrganizationUserType.Owner)]
|
|
||||||
[BitAutoData(PlanType.TeamsMonthly2019, OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(PlanType.TeamsMonthly2019, OrganizationUserType.Owner)]
|
|
||||||
public async Task ConfirmUserToNonFree_AlreadyFreeAdminOrOwner_DoesNotThrow(PlanType planType, OrganizationUserType orgUserType, Organization org, OrganizationUser confirmingUser,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
|
||||||
string key, SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
|
||||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
|
||||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
|
||||||
|
|
||||||
org.PlanType = planType;
|
|
||||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
|
||||||
orgUser.UserId = user.Id;
|
|
||||||
orgUser.Type = orgUserType;
|
|
||||||
orgUser.AccessSecretsManager = false;
|
|
||||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
|
||||||
organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(orgUser.UserId.Value).Returns(1);
|
|
||||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
|
||||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
|
||||||
|
|
||||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
|
||||||
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email);
|
|
||||||
await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task ConfirmUser_AsUser_SingleOrgPolicy_AppliedFromConfirmingOrg_Throws(Organization org, OrganizationUser confirmingUser,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
|
||||||
OrganizationUser orgUserAnotherOrg, [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,
|
|
||||||
string key, SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
|
||||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
|
||||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
|
||||||
var policyService = sutProvider.GetDependency<IPolicyService>();
|
|
||||||
|
|
||||||
org.PlanType = PlanType.EnterpriseAnnually;
|
|
||||||
orgUser.Status = OrganizationUserStatusType.Accepted;
|
|
||||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
|
||||||
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
|
||||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
|
||||||
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
|
||||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
|
||||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
|
||||||
singleOrgPolicy.OrganizationId = org.Id;
|
|
||||||
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy });
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
|
||||||
Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task ConfirmUser_AsUser_SingleOrgPolicy_AppliedFromOtherOrg_Throws(Organization org, OrganizationUser confirmingUser,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
|
||||||
OrganizationUser orgUserAnotherOrg, [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,
|
|
||||||
string key, SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
|
||||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
|
||||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
|
||||||
var policyService = sutProvider.GetDependency<IPolicyService>();
|
|
||||||
|
|
||||||
org.PlanType = PlanType.EnterpriseAnnually;
|
|
||||||
orgUser.Status = OrganizationUserStatusType.Accepted;
|
|
||||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
|
||||||
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
|
||||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
|
||||||
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
|
||||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
|
||||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
|
||||||
singleOrgPolicy.OrganizationId = orgUserAnotherOrg.Id;
|
|
||||||
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy });
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
|
||||||
Assert.Contains("Cannot confirm this member to the organization because they are in another organization which forbids it.", exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData(OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(OrganizationUserType.Owner)]
|
|
||||||
public async Task ConfirmUser_AsOwnerOrAdmin_SingleOrgPolicy_ExcludedViaUserType_Success(
|
|
||||||
OrganizationUserType userType, Organization org, OrganizationUser confirmingUser,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
|
||||||
OrganizationUser orgUserAnotherOrg,
|
|
||||||
string key, SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
|
||||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
|
||||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
|
||||||
|
|
||||||
org.PlanType = PlanType.EnterpriseAnnually;
|
|
||||||
orgUser.Type = userType;
|
|
||||||
orgUser.Status = OrganizationUserStatusType.Accepted;
|
|
||||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
|
||||||
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
|
||||||
orgUser.AccessSecretsManager = true;
|
|
||||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
|
||||||
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
|
||||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
|
||||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
|
||||||
|
|
||||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
|
||||||
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, true);
|
|
||||||
await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task ConfirmUser_TwoFactorPolicy_NotEnabled_Throws(Organization org, OrganizationUser confirmingUser,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
|
||||||
OrganizationUser orgUserAnotherOrg,
|
|
||||||
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
|
||||||
string key, SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
|
||||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
|
||||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
|
||||||
var policyService = sutProvider.GetDependency<IPolicyService>();
|
|
||||||
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
|
||||||
|
|
||||||
org.PlanType = PlanType.EnterpriseAnnually;
|
|
||||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
|
||||||
orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;
|
|
||||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
|
||||||
organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });
|
|
||||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
|
||||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
|
||||||
twoFactorPolicy.OrganizationId = org.Id;
|
|
||||||
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
|
||||||
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
|
|
||||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
|
||||||
Assert.Contains("User does not have two-step login enabled.", exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task ConfirmUser_TwoFactorPolicy_Enabled_Success(Organization org, OrganizationUser confirmingUser,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
|
||||||
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
|
||||||
string key, SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
|
||||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
|
||||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
|
||||||
var policyService = sutProvider.GetDependency<IPolicyService>();
|
|
||||||
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
|
||||||
|
|
||||||
org.PlanType = PlanType.EnterpriseAnnually;
|
|
||||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
|
||||||
orgUser.UserId = user.Id;
|
|
||||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
|
||||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
|
||||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
|
||||||
twoFactorPolicy.OrganizationId = org.Id;
|
|
||||||
policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
|
||||||
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
|
|
||||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, true) });
|
|
||||||
|
|
||||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task ConfirmUsers_Success(Organization org,
|
|
||||||
OrganizationUser confirmingUser,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser1,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser3,
|
|
||||||
OrganizationUser anotherOrgUser, User user1, User user2, User user3,
|
|
||||||
[OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,
|
|
||||||
[OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,
|
|
||||||
string key, SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
|
||||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
|
||||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
|
||||||
var policyService = sutProvider.GetDependency<IPolicyService>();
|
|
||||||
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
|
||||||
|
|
||||||
org.PlanType = PlanType.EnterpriseAnnually;
|
|
||||||
orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
|
||||||
orgUser1.UserId = user1.Id;
|
|
||||||
orgUser2.UserId = user2.Id;
|
|
||||||
orgUser3.UserId = user3.Id;
|
|
||||||
anotherOrgUser.UserId = user3.Id;
|
|
||||||
var orgUsers = new[] { orgUser1, orgUser2, orgUser3 };
|
|
||||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(orgUsers);
|
|
||||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
|
||||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user1, user2, user3 });
|
|
||||||
twoFactorPolicy.OrganizationId = org.Id;
|
|
||||||
policyService.GetPoliciesApplicableToUserAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
|
|
||||||
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id) && ids.Contains(user3.Id)))
|
|
||||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>()
|
|
||||||
{
|
|
||||||
(user1.Id, true),
|
|
||||||
(user2.Id, false),
|
|
||||||
(user3.Id, true)
|
|
||||||
});
|
|
||||||
singleOrgPolicy.OrganizationId = org.Id;
|
|
||||||
policyService.GetPoliciesApplicableToUserAsync(user3.Id, PolicyType.SingleOrg)
|
|
||||||
.Returns(new[] { singleOrgPolicy });
|
|
||||||
organizationUserRepository.GetManyByManyUsersAsync(default)
|
|
||||||
.ReturnsForAnyArgs(new[] { orgUser1, orgUser2, orgUser3, anotherOrgUser });
|
|
||||||
|
|
||||||
var keys = orgUsers.ToDictionary(ou => ou.Id, _ => key);
|
|
||||||
var result = await sutProvider.Sut.ConfirmUsersAsync(confirmingUser.OrganizationId, keys, confirmingUser.Id);
|
|
||||||
Assert.Contains("", result[0].Item2);
|
|
||||||
Assert.Contains("User does not have two-step login enabled.", result[1].Item2);
|
|
||||||
Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task UpdateOrganizationKeysAsync_WithoutManageResetPassword_Throws(Guid orgId, string publicKey,
|
public async Task UpdateOrganizationKeysAsync_WithoutManageResetPassword_Throws(Guid orgId, string publicKey,
|
||||||
string privateKey, SutProvider<OrganizationService> sutProvider)
|
string privateKey, SutProvider<OrganizationService> sutProvider)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using System.Text.Json;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -9,7 +10,9 @@ using Bit.Core.Services;
|
|||||||
using Bit.Core.Test.AutoFixture.CipherFixtures;
|
using Bit.Core.Test.AutoFixture.CipherFixtures;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
|
using Bit.Core.Vault.Enums;
|
||||||
using Bit.Core.Vault.Models.Data;
|
using Bit.Core.Vault.Models.Data;
|
||||||
|
using Bit.Core.Vault.Queries;
|
||||||
using Bit.Core.Vault.Repositories;
|
using Bit.Core.Vault.Repositories;
|
||||||
using Bit.Core.Vault.Services;
|
using Bit.Core.Vault.Services;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
@ -797,6 +800,233 @@ public class CipherServiceTests
|
|||||||
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class SaveDetailsAsyncDependencies
|
||||||
|
{
|
||||||
|
public CipherDetails CipherDetails { get; set; }
|
||||||
|
public SutProvider<CipherService> SutProvider { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SaveDetailsAsyncDependencies GetSaveDetailsAsyncDependencies(
|
||||||
|
SutProvider<CipherService> sutProvider,
|
||||||
|
string newPassword,
|
||||||
|
bool viewPassword,
|
||||||
|
bool editPermission,
|
||||||
|
string? key = null,
|
||||||
|
string? totp = null,
|
||||||
|
CipherLoginFido2CredentialData[]? passkeys = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var cipherDetails = new CipherDetails
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
OrganizationId = Guid.NewGuid(),
|
||||||
|
Type = CipherType.Login,
|
||||||
|
UserId = Guid.NewGuid(),
|
||||||
|
RevisionDate = DateTime.UtcNow,
|
||||||
|
Key = key,
|
||||||
|
};
|
||||||
|
|
||||||
|
var newLoginData = new CipherLoginData { Username = "user", Password = newPassword, Totp = totp, Fido2Credentials = passkeys };
|
||||||
|
cipherDetails.Data = JsonSerializer.Serialize(newLoginData);
|
||||||
|
|
||||||
|
var existingCipher = new Cipher
|
||||||
|
{
|
||||||
|
Id = cipherDetails.Id,
|
||||||
|
Data = JsonSerializer.Serialize(
|
||||||
|
new CipherLoginData
|
||||||
|
{
|
||||||
|
Username = "user",
|
||||||
|
Password = "OriginalPassword",
|
||||||
|
Totp = "OriginalTotp",
|
||||||
|
Fido2Credentials = []
|
||||||
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICipherRepository>()
|
||||||
|
.GetByIdAsync(cipherDetails.Id)
|
||||||
|
.Returns(existingCipher);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICipherRepository>()
|
||||||
|
.ReplaceAsync(Arg.Any<CipherDetails>())
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var permissions = new Dictionary<Guid, OrganizationCipherPermission>
|
||||||
|
{
|
||||||
|
{ cipherDetails.Id, new OrganizationCipherPermission { ViewPassword = viewPassword, Edit = editPermission } }
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IGetCipherPermissionsForUserQuery>()
|
||||||
|
.GetByOrganization(cipherDetails.OrganizationId.Value)
|
||||||
|
.Returns(permissions);
|
||||||
|
|
||||||
|
return new SaveDetailsAsyncDependencies
|
||||||
|
{
|
||||||
|
CipherDetails = cipherDetails,
|
||||||
|
SutProvider = sutProvider,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_PasswordNotChangedWithoutViewPasswordPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: false, editPermission: true);
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||||
|
Assert.Equal("OriginalPassword", updatedLoginData.Password);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_PasswordNotChangedWithoutEditPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false);
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||||
|
Assert.Equal("OriginalPassword", updatedLoginData.Password);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_PasswordChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true);
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||||
|
Assert.Equal("NewPassword", updatedLoginData.Password);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_CipherKeyChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, "NewKey");
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
Assert.Equal("NewKey", deps.CipherDetails.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_CipherKeyChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, "NewKey");
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true));
|
||||||
|
|
||||||
|
Assert.Contains("do not have permission", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_TotpChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, totp: "NewTotp");
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||||
|
Assert.Equal("OriginalTotp", updatedLoginData.Totp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_TotpChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, totp: "NewTotp");
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||||
|
Assert.Equal("NewTotp", updatedLoginData.Totp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_Fido2CredentialsChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var passkeys = new[]
|
||||||
|
{
|
||||||
|
new CipherLoginFido2CredentialData
|
||||||
|
{
|
||||||
|
CredentialId = "CredentialId",
|
||||||
|
UserHandle = "UserHandle",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, passkeys: passkeys);
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||||
|
Assert.Empty(updatedLoginData.Fido2Credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_Fido2CredentialsChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
var passkeys = new[]
|
||||||
|
{
|
||||||
|
new CipherLoginFido2CredentialData
|
||||||
|
{
|
||||||
|
CredentialId = "CredentialId",
|
||||||
|
UserHandle = "UserHandle",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, passkeys: passkeys);
|
||||||
|
|
||||||
|
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||||
|
deps.CipherDetails,
|
||||||
|
deps.CipherDetails.UserId.Value,
|
||||||
|
deps.CipherDetails.RevisionDate,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
var updatedLoginData = JsonSerializer.Deserialize<CipherLoginData>(deps.CipherDetails.Data);
|
||||||
|
Assert.Equal(passkeys.Length, updatedLoginData.Fido2Credentials.Length);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task DeleteAsync_WithPersonalCipherOwner_DeletesCipher(
|
public async Task DeleteAsync_WithPersonalCipherOwner_DeletesCipher(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user