+ style="display: table; width:100%; padding-bottom: 24px; text-align: center;" align="center">
+ 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
|
+
+
+
+ {{formatAdminOwnerEmails AdminOwnerEmails}}
+ |
+
{{/SecurityTasksHtmlLayout}}
diff --git a/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs b/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs
index ba8650ad10..f5493e4503 100644
--- a/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs
+++ b/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs
@@ -5,4 +5,13 @@ breach.
Launch the Bitwarden extension to review your at-risk passwords.
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}}
diff --git a/src/Core/Models/Mail/SecurityTaskNotificationViewModel.cs b/src/Core/Models/Mail/SecurityTaskNotificationViewModel.cs
index 7f93ac2439..8871a53424 100644
--- a/src/Core/Models/Mail/SecurityTaskNotificationViewModel.cs
+++ b/src/Core/Models/Mail/SecurityTaskNotificationViewModel.cs
@@ -8,5 +8,7 @@ public class SecurityTaskNotificationViewModel : BaseMailModel
public bool TaskCountPlural => TaskCount != 1;
+ public IEnumerable AdminOwnerEmails { get; set; }
+
public string ReviewPasswordsUrl => $"{WebVaultUrl}/browser-extension-prompt";
}
diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs
index 232e04fbd0..e13a06f660 100644
--- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs
+++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs
@@ -116,6 +116,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
}
private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services)
diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs
index b0b884eb3e..e61127c57a 100644
--- a/src/Core/Services/IMailService.cs
+++ b/src/Core/Services/IMailService.cs
@@ -99,5 +99,5 @@ public interface IMailService
string organizationName);
Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList);
Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable adminEmails, Guid organizationId, string email, string userName);
- Task SendBulkSecurityTaskNotificationsAsync(string orgName, IEnumerable securityTaskNotificaitons);
+ Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable securityTaskNotifications, IEnumerable adminOwnerEmails);
}
diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs
index d96f69b0a6..edb99809f7 100644
--- a/src/Core/Services/Implementations/HandlebarsMailService.cs
+++ b/src/Core/Services/Implementations/HandlebarsMailService.cs
@@ -740,6 +740,45 @@ public class HandlebarsMailService : IMailService
var clickTrackingText = (clickTrackingOff ? "clicktracking=off" : string.Empty);
writer.WriteSafeString($"{text}");
});
+
+ // 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)parameters[0]).ToList();
+ if (emailList.Count == 0)
+ {
+ writer.WriteSafeString(string.Empty);
+ return;
+ }
+
+ string constructAnchorElement(string email)
+ {
+ return $"{email}";
+ }
+
+ 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)
@@ -1201,21 +1240,23 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message);
}
- public async Task SendBulkSecurityTaskNotificationsAsync(string orgName, IEnumerable securityTaskNotificaitons)
+ public async Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable securityTaskNotifications, IEnumerable adminOwnerEmails)
{
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
{
- OrgName = orgName,
+ OrgName = CoreHelpers.SanitizeForEmail(sanitizedOrgName, false),
TaskCount = notification.TaskCount,
+ AdminOwnerEmails = adminOwnerEmails,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
};
message.Category = "SecurityTasksNotification";
return new MailQueueMessage(message, "SecurityTasksNotification", model);
}
- var messageModels = securityTaskNotificaitons.Select(CreateMessage);
+ var messageModels = securityTaskNotifications.Select(CreateMessage);
await EnqueueMailAsync(messageModels.ToList());
}
diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs
index 5fba545903..d829fbbacb 100644
--- a/src/Core/Services/NoopImplementations/NoopMailService.cs
+++ b/src/Core/Services/NoopImplementations/NoopMailService.cs
@@ -324,7 +324,7 @@ public class NoopMailService : IMailService
return Task.FromResult(0);
}
- public Task SendBulkSecurityTaskNotificationsAsync(string orgName, IEnumerable securityTaskNotificaitons)
+ public Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable securityTaskNotifications, IEnumerable adminOwnerEmails)
{
return Task.FromResult(0);
}
diff --git a/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs b/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs
index 58b5f65e0f..a335b059a4 100644
--- a/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs
+++ b/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs
@@ -17,19 +17,22 @@ public class CreateManyTaskNotificationsCommand : ICreateManyTaskNotificationsCo
private readonly IMailService _mailService;
private readonly ICreateNotificationCommand _createNotificationCommand;
private readonly IPushNotificationService _pushNotificationService;
+ private readonly IOrganizationUserRepository _organizationUserRepository;
public CreateManyTaskNotificationsCommand(
IGetSecurityTasksNotificationDetailsQuery getSecurityTasksNotificationDetailsQuery,
IOrganizationRepository organizationRepository,
IMailService mailService,
ICreateNotificationCommand createNotificationCommand,
- IPushNotificationService pushNotificationService)
+ IPushNotificationService pushNotificationService,
+ IOrganizationUserRepository organizationUserRepository)
{
_getSecurityTasksNotificationDetailsQuery = getSecurityTasksNotificationDetailsQuery;
_organizationRepository = organizationRepository;
_mailService = mailService;
_createNotificationCommand = createNotificationCommand;
_pushNotificationService = pushNotificationService;
+ _organizationUserRepository = organizationUserRepository;
}
public async Task CreateAsync(Guid orgId, IEnumerable securityTasks)
@@ -45,8 +48,11 @@ public class CreateManyTaskNotificationsCommand : ICreateManyTaskNotificationsCo
}).ToList();
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
var securityTaskCiphersByUser = securityTaskCiphers.GroupBy(x => x.UserId)
diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs
index 90c03df90b..a315528e59 100644
--- a/src/Core/Vault/Services/Implementations/CipherService.cs
+++ b/src/Core/Vault/Services/Implementations/CipherService.cs
@@ -13,7 +13,9 @@ using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Bit.Core.Vault.Entities;
+using Bit.Core.Vault.Enums;
using Bit.Core.Vault.Models.Data;
+using Bit.Core.Vault.Queries;
using Bit.Core.Vault.Repositories;
namespace Bit.Core.Vault.Services;
@@ -38,6 +40,7 @@ public class CipherService : ICipherService
private const long _fileSizeLeeway = 1024L * 1024L; // 1MB
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;
+ private readonly IGetCipherPermissionsForUserQuery _getCipherPermissionsForUserQuery;
public CipherService(
ICipherRepository cipherRepository,
@@ -54,7 +57,8 @@ public class CipherService : ICipherService
IPolicyService policyService,
GlobalSettings globalSettings,
IReferenceEventService referenceEventService,
- ICurrentContext currentContext)
+ ICurrentContext currentContext,
+ IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery)
{
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
@@ -71,6 +75,7 @@ public class CipherService : ICipherService
_globalSettings = globalSettings;
_referenceEventService = referenceEventService;
_currentContext = currentContext;
+ _getCipherPermissionsForUserQuery = getCipherPermissionsForUserQuery;
}
public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
@@ -161,6 +166,7 @@ public class CipherService : ICipherService
{
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
cipher.RevisionDate = DateTime.UtcNow;
+ await ValidateViewPasswordUserAsync(cipher);
await _cipherRepository.ReplaceAsync(cipher);
await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_Updated);
@@ -966,4 +972,32 @@ public class CipherService : ICipherService
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(existingCipher.Data);
+ var newCipherData = JsonSerializer.Deserialize(cipher.Data);
+ newCipherData.Fido2Credentials = existingCipherData.Fido2Credentials;
+ newCipherData.Totp = existingCipherData.Totp;
+ newCipherData.Password = existingCipherData.Password;
+ cipher.Data = JsonSerializer.Serialize(newCipherData);
+ }
+ }
}
diff --git a/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj b/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj
index 19512670ce..c51af39824 100644
--- a/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj
+++ b/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj
@@ -2,7 +2,7 @@
- $(WarningsNotAsErrors);CS8618;CS4014
+ $(WarningsNotAsErrors);CS8618
diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs
index e3071bd227..a19560ecee 100644
--- a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs
+++ b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs
@@ -7,6 +7,8 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
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.Auth.Entities;
using Bit.Core.Auth.Repositories;
@@ -424,4 +426,93 @@ public class OrganizationUsersControllerTests
.GetManyDetailsByOrganizationAsync(organizationAbility.Id, Arg.Any(), Arg.Any())
.Returns(organizationUsers);
}
+
+ [Theory]
+ [BitAutoData]
+ public async Task Accept_WhenOrganizationUsePoliciesIsEnabledAndResetPolicyIsEnabled_WithPolicyRequirementsEnabled_ShouldHandleResetPassword(Guid orgId, Guid orgUserId,
+ OrganizationUserAcceptRequestModel model, User user, SutProvider sutProvider)
+ {
+ // Arrange
+ var applicationCacheService = sutProvider.GetDependency();
+ applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = true });
+
+ sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
+
+ var policy = new Policy
+ {
+ Enabled = true,
+ Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }),
+ };
+ var userService = sutProvider.GetDependency();
+ userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
+
+ var policyRequirementQuery = sutProvider.GetDependency();
+
+ var policyRepository = sutProvider.GetDependency();
+
+ var policyRequirement = new ResetPasswordPolicyRequirement { AutoEnrollOrganizations = [orgId] };
+
+ policyRequirementQuery.GetAsync(user.Id).Returns(policyRequirement);
+
+ // Act
+ await sutProvider.Sut.Accept(orgId, orgUserId, model);
+
+ // Assert
+ await sutProvider.GetDependency().Received(1)
+ .AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService);
+ await sutProvider.GetDependency().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(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 sutProvider)
+ {
+ // Arrange
+ model.ResetPasswordKey = " ";
+ var applicationCacheService = sutProvider.GetDependency();
+ applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = true });
+
+ sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
+
+ var policy = new Policy
+ {
+ Enabled = true,
+ Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }),
+ };
+ var userService = sutProvider.GetDependency();
+ userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
+
+ var policyRepository = sutProvider.GetDependency();
+
+ var policyRequirementQuery = sutProvider.GetDependency();
+
+ var policyRequirement = new ResetPasswordPolicyRequirement { AutoEnrollOrganizations = [orgId] };
+
+ policyRequirementQuery.GetAsync(user.Id).Returns(policyRequirement);
+
+ // Act
+ var exception = await Assert.ThrowsAsync(() =>
+ sutProvider.Sut.Accept(orgId, orgUserId, model));
+
+ // Assert
+ await sutProvider.GetDependency().Received(0)
+ .AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService);
+ await sutProvider.GetDependency().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(user.Id);
+
+ Assert.Equal("Master Password reset is required, but not provided.", exception.Message);
+ }
}
diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs
index b0906ddc43..8e6d2ce27b 100644
--- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs
+++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs
@@ -4,12 +4,15 @@ using Bit.Api.AdminConsole.Controllers;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
+using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.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.Auth.Entities;
using Bit.Core.Auth.Enums;
@@ -55,6 +58,7 @@ public class OrganizationsControllerTests : IDisposable
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
+ private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly IPricingClient _pricingClient;
private readonly OrganizationsController _sut;
@@ -80,6 +84,7 @@ public class OrganizationsControllerTests : IDisposable
_removeOrganizationUserCommand = Substitute.For();
_cloudOrganizationSignUpCommand = Substitute.For();
_organizationDeleteCommand = Substitute.For();
+ _policyRequirementQuery = Substitute.For();
_pricingClient = Substitute.For();
_sut = new OrganizationsController(
@@ -103,6 +108,7 @@ public class OrganizationsControllerTests : IDisposable
_removeOrganizationUserCommand,
_cloudOrganizationSignUpCommand,
_organizationDeleteCommand,
+ _policyRequirementQuery,
_pricingClient);
}
@@ -236,4 +242,55 @@ public class OrganizationsControllerTests : IDisposable
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()).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(user.Id).Returns(policyRequirement);
+
+ var result = await _sut.GetAutoEnrollStatus(organization.Id.ToString());
+
+ await _userService.Received(1).GetUserByPrincipalAsync(Arg.Any());
+ await _organizationRepository.Received(1).GetByIdentifierAsync(organization.Id.ToString());
+ await _policyRequirementQuery.Received(1).GetAsync(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()).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());
+ await _organizationRepository.Received(1).GetByIdentifierAsync(organization.Id.ToString());
+ await _policyRequirementQuery.Received(0).GetAsync(user.Id);
+ await _policyRepository.Received(1).GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
+
+ Assert.True(result.ResetPasswordEnabled);
+ }
}
diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs
new file mode 100644
index 0000000000..06335f668d
--- /dev/null
+++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs
@@ -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 sutProvider)
+ {
+ var organizationUserRepository = sutProvider.GetDependency();
+
+ organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser);
+
+ var exception = await Assert.ThrowsAsync(
+ () => 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 sutProvider)
+ {
+ var organizationUserRepository = sutProvider.GetDependency();
+
+ organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser);
+
+ var exception = await Assert.ThrowsAsync(
+ () => 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 sutProvider)
+ {
+ var organizationUserRepository = sutProvider.GetDependency();
+ var organizationRepository = sutProvider.GetDependency();
+ var userRepository = sutProvider.GetDependency();
+
+ 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(
+ () => 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 sutProvider)
+ {
+ var organizationUserRepository = sutProvider.GetDependency();
+ var organizationRepository = sutProvider.GetDependency();
+ var userRepository = sutProvider.GetDependency();
+
+ 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().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
+ await sutProvider.GetDependency().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email);
+ await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is>(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 sutProvider)
+ {
+ var organizationUserRepository = sutProvider.GetDependency();
+ var organizationRepository = sutProvider.GetDependency();
+ var userRepository = sutProvider.GetDependency();
+ var policyService = sutProvider.GetDependency();
+
+ 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(
+ () => 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 sutProvider)
+ {
+ var organizationUserRepository = sutProvider.GetDependency();
+ var organizationRepository = sutProvider.GetDependency();
+ var userRepository = sutProvider.GetDependency();
+ var policyService = sutProvider.GetDependency();
+
+ 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(
+ () => 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 sutProvider)
+ {
+ var organizationUserRepository = sutProvider.GetDependency();
+ var organizationRepository = sutProvider.GetDependency();
+ var userRepository = sutProvider.GetDependency();
+
+ 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().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
+ await sutProvider.GetDependency().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, true);
+ await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is>(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 sutProvider)
+ {
+ var organizationUserRepository = sutProvider.GetDependency();
+ var organizationRepository = sutProvider.GetDependency();
+ var userRepository = sutProvider.GetDependency();
+ var policyService = sutProvider.GetDependency();
+ var twoFactorIsEnabledQuery = sutProvider.GetDependency();
+
+ 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>(ids => ids.Contains(user.Id)))
+ .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });
+
+ var exception = await Assert.ThrowsAsync(
+ () => 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 sutProvider)
+ {
+ var organizationUserRepository = sutProvider.GetDependency();
+ var organizationRepository = sutProvider.GetDependency();
+ var userRepository = sutProvider.GetDependency();
+ var policyService = sutProvider.GetDependency();
+ var twoFactorIsEnabledQuery = sutProvider.GetDependency();
+
+ 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>(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 sutProvider)
+ {
+ var organizationUserRepository = sutProvider.GetDependency();
+ var organizationRepository = sutProvider.GetDependency();
+ var userRepository = sutProvider.GetDependency();
+ var policyService = sutProvider.GetDependency();
+ var twoFactorIsEnabledQuery = sutProvider.GetDependency();
+
+ 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(), PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
+ twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is>(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);
+ }
+}
diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/ResetPasswordPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/ResetPasswordPolicyRequirementFactoryTests.cs
new file mode 100644
index 0000000000..181f4f170e
--- /dev/null
+++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/ResetPasswordPolicyRequirementFactoryTests.cs
@@ -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 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 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));
+ }
+}
diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs
index 4c42fdfeb9..82dc0e2ebe 100644
--- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs
+++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs
@@ -24,7 +24,6 @@ using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
-using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
using Bit.Core.Tokens;
@@ -978,306 +977,6 @@ OrganizationUserInvite invite, SutProvider sutProvider)
sutProvider.GetDependency().ManageUsers(organization.Id).Returns(true);
}
- [Theory, BitAutoData]
- public async Task ConfirmUser_InvalidStatus(OrganizationUser confirmingUser,
- [OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser orgUser, string key,
- SutProvider sutProvider)
- {
- var organizationUserRepository = sutProvider.GetDependency();
-
- organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser);
-
- var exception = await Assert.ThrowsAsync(
- () => 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 sutProvider)
- {
- var organizationUserRepository = sutProvider.GetDependency();
-
- organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser);
-
- var exception = await Assert.ThrowsAsync(
- () => 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 sutProvider)
- {
- var organizationUserRepository = sutProvider.GetDependency();
- var organizationRepository = sutProvider.GetDependency();
- var userRepository = sutProvider.GetDependency();
-
- 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(
- () => 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 sutProvider)
- {
- var organizationUserRepository = sutProvider.GetDependency();
- var organizationRepository = sutProvider.GetDependency();
- var userRepository = sutProvider.GetDependency();
-
- 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().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
- await sutProvider.GetDependency().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email);
- await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is>(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 sutProvider)
- {
- var organizationUserRepository = sutProvider.GetDependency();
- var organizationRepository = sutProvider.GetDependency();
- var userRepository = sutProvider.GetDependency();
- var policyService = sutProvider.GetDependency();
-
- 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(
- () => 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 sutProvider)
- {
- var organizationUserRepository = sutProvider.GetDependency();
- var organizationRepository = sutProvider.GetDependency();
- var userRepository = sutProvider.GetDependency();
- var policyService = sutProvider.GetDependency();
-
- 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(
- () => 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 sutProvider)
- {
- var organizationUserRepository = sutProvider.GetDependency();
- var organizationRepository = sutProvider.GetDependency();
- var userRepository = sutProvider.GetDependency();
-
- 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().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
- await sutProvider.GetDependency().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, true);
- await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is>(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 sutProvider)
- {
- var organizationUserRepository = sutProvider.GetDependency();
- var organizationRepository = sutProvider.GetDependency();
- var userRepository = sutProvider.GetDependency();
- var policyService = sutProvider.GetDependency();
- var twoFactorIsEnabledQuery = sutProvider.GetDependency();
-
- 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>(ids => ids.Contains(user.Id)))
- .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });
-
- var exception = await Assert.ThrowsAsync(
- () => 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 sutProvider)
- {
- var organizationUserRepository = sutProvider.GetDependency();
- var organizationRepository = sutProvider.GetDependency();
- var userRepository = sutProvider.GetDependency();
- var policyService = sutProvider.GetDependency();
- var twoFactorIsEnabledQuery = sutProvider.GetDependency();
-
- 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>(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 sutProvider)
- {
- var organizationUserRepository = sutProvider.GetDependency();
- var organizationRepository = sutProvider.GetDependency();
- var userRepository = sutProvider.GetDependency();
- var policyService = sutProvider.GetDependency();
- var twoFactorIsEnabledQuery = sutProvider.GetDependency();
-
- 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(), PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });
- twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is>(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]
public async Task UpdateOrganizationKeysAsync_WithoutManageResetPassword_Throws(Guid orgId, string publicKey,
string privateKey, SutProvider sutProvider)
diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs
index 1803c980c2..3ef29146c2 100644
--- a/test/Core.Test/Vault/Services/CipherServiceTests.cs
+++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs
@@ -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.Entities;
using Bit.Core.Enums;
@@ -9,7 +10,9 @@ using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.CipherFixtures;
using Bit.Core.Utilities;
using Bit.Core.Vault.Entities;
+using Bit.Core.Vault.Enums;
using Bit.Core.Vault.Models.Data;
+using Bit.Core.Vault.Queries;
using Bit.Core.Vault.Repositories;
using Bit.Core.Vault.Services;
using Bit.Test.Common.AutoFixture;
@@ -797,6 +800,233 @@ public class CipherServiceTests
Arg.Is>(arg => !arg.Except(ciphers).Any()));
}
+ private class SaveDetailsAsyncDependencies
+ {
+ public CipherDetails CipherDetails { get; set; }
+ public SutProvider SutProvider { get; set; }
+ }
+
+ private static SaveDetailsAsyncDependencies GetSaveDetailsAsyncDependencies(
+ SutProvider 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()
+ .GetByIdAsync(cipherDetails.Id)
+ .Returns(existingCipher);
+
+ sutProvider.GetDependency()
+ .ReplaceAsync(Arg.Any())
+ .Returns(Task.CompletedTask);
+
+ var permissions = new Dictionary
+ {
+ { cipherDetails.Id, new OrganizationCipherPermission { ViewPassword = viewPassword, Edit = editPermission } }
+ };
+
+ sutProvider.GetDependency()
+ .GetByOrganization(cipherDetails.OrganizationId.Value)
+ .Returns(permissions);
+
+ return new SaveDetailsAsyncDependencies
+ {
+ CipherDetails = cipherDetails,
+ SutProvider = sutProvider,
+ };
+ }
+
+ [Theory, BitAutoData]
+ public async Task SaveDetailsAsync_PasswordNotChangedWithoutViewPasswordPermission(string _, SutProvider 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(deps.CipherDetails.Data);
+ Assert.Equal("OriginalPassword", updatedLoginData.Password);
+ }
+
+ [Theory, BitAutoData]
+ public async Task SaveDetailsAsync_PasswordNotChangedWithoutEditPermission(string _, SutProvider 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(deps.CipherDetails.Data);
+ Assert.Equal("OriginalPassword", updatedLoginData.Password);
+ }
+
+ [Theory, BitAutoData]
+ public async Task SaveDetailsAsync_PasswordChangedWithPermission(string _, SutProvider 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(deps.CipherDetails.Data);
+ Assert.Equal("NewPassword", updatedLoginData.Password);
+ }
+
+ [Theory, BitAutoData]
+ public async Task SaveDetailsAsync_CipherKeyChangedWithPermission(string _, SutProvider 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 sutProvider)
+ {
+ var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, "NewKey");
+
+ var exception = await Assert.ThrowsAsync(() => 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 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(deps.CipherDetails.Data);
+ Assert.Equal("OriginalTotp", updatedLoginData.Totp);
+ }
+
+ [Theory, BitAutoData]
+ public async Task SaveDetailsAsync_TotpChangedWithPermission(string _, SutProvider 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(deps.CipherDetails.Data);
+ Assert.Equal("NewTotp", updatedLoginData.Totp);
+ }
+
+ [Theory, BitAutoData]
+ public async Task SaveDetailsAsync_Fido2CredentialsChangedWithoutPermission(string _, SutProvider 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(deps.CipherDetails.Data);
+ Assert.Empty(updatedLoginData.Fido2Credentials);
+ }
+
+ [Theory, BitAutoData]
+ public async Task SaveDetailsAsync_Fido2CredentialsChangedWithPermission(string _, SutProvider 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(deps.CipherDetails.Data);
+ Assert.Equal(passkeys.Length, updatedLoginData.Fido2Credentials.Length);
+ }
+
[Theory]
[BitAutoData]
public async Task DeleteAsync_WithPersonalCipherOwner_DeletesCipher(