1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 13:08:17 -05:00

Merge branch 'main' into jmccannon/ac/pm-12487-restore-user-command

This commit is contained in:
jrmccannon 2025-03-20 09:33:25 -05:00
commit 41d0a4563b
No known key found for this signature in database
GPG Key ID: CF03F3DB01CE96A6
8 changed files with 282 additions and 11 deletions

View File

@ -127,9 +127,9 @@ public class CiphersController : Controller
public async Task<ListResponseModel<CipherDetailsResponseModel>> Get()
{
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?
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;
if (hasOrgs)
{

View File

@ -118,6 +118,7 @@ public static class FeatureFlagKeys
public const string ExportAttachments = "export-attachments";
/* 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 NewDeviceVerificationPermanentDismiss = "new-device-permanent-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 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 UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
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 PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
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()
{

View File

@ -99,5 +99,5 @@ public interface IMailService
string organizationName);
Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList);
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);
}

View File

@ -1201,21 +1201,22 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendBulkSecurityTaskNotificationsAsync(string orgName, IEnumerable<UserSecurityTasksCount> securityTaskNotificaitons)
public async Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications)
{
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,
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());
}

View File

@ -324,7 +324,7 @@ public class NoopMailService : IMailService
return Task.FromResult(0);
}
public Task SendBulkSecurityTaskNotificationsAsync(string orgName, IEnumerable<UserSecurityTasksCount> securityTaskNotificaitons)
public Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications)
{
return Task.FromResult(0);
}

View File

@ -46,7 +46,7 @@ public class CreateManyTaskNotificationsCommand : ICreateManyTaskNotificationsCo
var organization = await _organizationRepository.GetByIdAsync(orgId);
await _mailService.SendBulkSecurityTaskNotificationsAsync(organization.Name, userTaskCount);
await _mailService.SendBulkSecurityTaskNotificationsAsync(organization, userTaskCount);
// Break securityTaskCiphers into separate lists by user Id
var securityTaskCiphersByUser = securityTaskCiphers.GroupBy(x => x.UserId)

View File

@ -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<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);
}
}
}

View File

@ -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<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]
[BitAutoData]
public async Task DeleteAsync_WithPersonalCipherOwner_DeletesCipher(