From 87cdb923a5a3bf1b0a23aff4641728505dd9563b Mon Sep 17 00:00:00 2001
From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
Date: Tue, 18 Mar 2025 11:44:36 -0400
Subject: [PATCH 1/9] [PM-17901] Replaced hard-coded Bitwarden Vault URLs
(#5458)
* Replaced hard-coded Bitwarden Vault URLs
* Jared's feedback
---
...nizationUserRevokedForSingleOrgPolicy.html.hbs | 2 +-
...nizationUserRevokedForSingleOrgPolicy.text.hbs | 2 +-
.../OrganizationSeatsAutoscaled.html.hbs | 2 +-
.../OrganizationSeatsMaxReached.html.hbs | 2 +-
.../OrganizationSmSeatsMaxReached.html.hbs | 2 +-
...ganizationSmServiceAccountsMaxReached.html.hbs | 2 +-
.../Mail/OrganizationSeatsAutoscaledViewModel.cs | 2 +-
.../Mail/OrganizationSeatsMaxReachedViewModel.cs | 2 +-
...anizationServiceAccountsMaxReachedViewModel.cs | 2 +-
.../Implementations/HandlebarsMailService.cs | 15 +++++++++++----
10 files changed, 20 insertions(+), 13 deletions(-)
diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForSingleOrgPolicy.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForSingleOrgPolicy.html.hbs
index d04abe86c9..5b2b1a70c5 100644
--- a/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForSingleOrgPolicy.html.hbs
+++ b/src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForSingleOrgPolicy.html.hbs
@@ -7,7 +7,7 @@
-
+
Manage subscription
diff --git a/src/Core/Models/Mail/OrganizationSeatsAutoscaledViewModel.cs b/src/Core/Models/Mail/OrganizationSeatsAutoscaledViewModel.cs
index 87f87b1c69..425b853d3e 100644
--- a/src/Core/Models/Mail/OrganizationSeatsAutoscaledViewModel.cs
+++ b/src/Core/Models/Mail/OrganizationSeatsAutoscaledViewModel.cs
@@ -2,7 +2,7 @@
public class OrganizationSeatsAutoscaledViewModel : BaseMailModel
{
- public Guid OrganizationId { get; set; }
public int InitialSeatCount { get; set; }
public int CurrentSeatCount { get; set; }
+ public string VaultSubscriptionUrl { get; set; }
}
diff --git a/src/Core/Models/Mail/OrganizationSeatsMaxReachedViewModel.cs b/src/Core/Models/Mail/OrganizationSeatsMaxReachedViewModel.cs
index cdfb57b2dc..ad9c48ab31 100644
--- a/src/Core/Models/Mail/OrganizationSeatsMaxReachedViewModel.cs
+++ b/src/Core/Models/Mail/OrganizationSeatsMaxReachedViewModel.cs
@@ -2,6 +2,6 @@
public class OrganizationSeatsMaxReachedViewModel : BaseMailModel
{
- public Guid OrganizationId { get; set; }
public int MaxSeatCount { get; set; }
+ public string VaultSubscriptionUrl { get; set; }
}
diff --git a/src/Core/Models/Mail/OrganizationServiceAccountsMaxReachedViewModel.cs b/src/Core/Models/Mail/OrganizationServiceAccountsMaxReachedViewModel.cs
index 1b9c925720..c814a3e564 100644
--- a/src/Core/Models/Mail/OrganizationServiceAccountsMaxReachedViewModel.cs
+++ b/src/Core/Models/Mail/OrganizationServiceAccountsMaxReachedViewModel.cs
@@ -2,6 +2,6 @@
public class OrganizationServiceAccountsMaxReachedViewModel
{
- public Guid OrganizationId { get; set; }
public int MaxServiceAccountsCount { get; set; }
+ public string VaultSubscriptionUrl { get; set; }
}
diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs
index c598a9d432..d96f69b0a6 100644
--- a/src/Core/Services/Implementations/HandlebarsMailService.cs
+++ b/src/Core/Services/Implementations/HandlebarsMailService.cs
@@ -214,9 +214,9 @@ public class HandlebarsMailService : IMailService
var message = CreateDefaultMessage($"{organization.DisplayName()} Seat Count Has Increased", ownerEmails);
var model = new OrganizationSeatsAutoscaledViewModel
{
- OrganizationId = organization.Id,
InitialSeatCount = initialSeatCount,
CurrentSeatCount = organization.Seats.Value,
+ VaultSubscriptionUrl = GetCloudVaultSubscriptionUrl(organization.Id)
};
await AddMessageContentAsync(message, "OrganizationSeatsAutoscaled", model);
@@ -229,8 +229,8 @@ public class HandlebarsMailService : IMailService
var message = CreateDefaultMessage($"{organization.DisplayName()} Seat Limit Reached", ownerEmails);
var model = new OrganizationSeatsMaxReachedViewModel
{
- OrganizationId = organization.Id,
MaxSeatCount = maxSeatCount,
+ VaultSubscriptionUrl = GetCloudVaultSubscriptionUrl(organization.Id)
};
await AddMessageContentAsync(message, "OrganizationSeatsMaxReached", model);
@@ -1103,8 +1103,8 @@ public class HandlebarsMailService : IMailService
var message = CreateDefaultMessage($"{organization.DisplayName()} Secrets Manager Seat Limit Reached", ownerEmails);
var model = new OrganizationSeatsMaxReachedViewModel
{
- OrganizationId = organization.Id,
MaxSeatCount = maxSeatCount,
+ VaultSubscriptionUrl = GetCloudVaultSubscriptionUrl(organization.Id)
};
await AddMessageContentAsync(message, "OrganizationSmSeatsMaxReached", model);
@@ -1118,8 +1118,8 @@ public class HandlebarsMailService : IMailService
var message = CreateDefaultMessage($"{organization.DisplayName()} Secrets Manager Machine Accounts Limit Reached", ownerEmails);
var model = new OrganizationServiceAccountsMaxReachedViewModel
{
- OrganizationId = organization.Id,
MaxServiceAccountsCount = maxSeatCount,
+ VaultSubscriptionUrl = GetCloudVaultSubscriptionUrl(organization.Id)
};
await AddMessageContentAsync(message, "OrganizationSmServiceAccountsMaxReached", model);
@@ -1223,4 +1223,11 @@ public class HandlebarsMailService : IMailService
{
return string.IsNullOrEmpty(userName) ? email : CoreHelpers.SanitizeForEmail(userName, false);
}
+
+ private string GetCloudVaultSubscriptionUrl(Guid organizationId)
+ => _globalSettings.BaseServiceUri.CloudRegion?.ToLower() switch
+ {
+ "eu" => $"https://vault.bitwarden.eu/#/organizations/{organizationId}/billing/subscription",
+ _ => $"https://vault.bitwarden.com/#/organizations/{organizationId}/billing/subscription"
+ };
}
From 508bf2c9f846de1d184acd65d6dab1ccea19b9fc Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 18 Mar 2025 14:26:29 -0400
Subject: [PATCH 2/9] [deps] Vault: Update AngleSharp to 1.2.0 (#5220)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
src/Icons/Icons.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Icons/Icons.csproj b/src/Icons/Icons.csproj
index 1674e2f877..455c8b3155 100644
--- a/src/Icons/Icons.csproj
+++ b/src/Icons/Icons.csproj
@@ -7,7 +7,7 @@
-
+
From 7f0dd6d1c320bb9f8ec336cad11521ae98150fa0 Mon Sep 17 00:00:00 2001
From: Vince Grassia <593223+vgrassia@users.noreply.github.com>
Date: Tue, 18 Mar 2025 13:02:39 -0700
Subject: [PATCH 3/9] Update FROM directive in Dockerfile (#5522)
---
util/Attachments/Dockerfile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/util/Attachments/Dockerfile b/util/Attachments/Dockerfile
index 2d99aa5911..37b23a1b95 100644
--- a/util/Attachments/Dockerfile
+++ b/util/Attachments/Dockerfile
@@ -1,4 +1,4 @@
-FROM bitwarden/server:latest
+FROM ghcr.io/bitwarden/server
LABEL com.bitwarden.product="bitwarden"
From bb3ec6aca13b691120b052747f6b4198e639d97d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rui=20Tom=C3=A9?=
<108268980+r-tome@users.noreply.github.com>
Date: Wed, 19 Mar 2025 11:01:06 +0000
Subject: [PATCH 4/9] [PM-16888] Refactor OrganizationUser status update
procedure to use a GuidIdArray parameter and remove JSON parsing logic
(#5237)
* Refactor OrganizationUser status update procedure to use a GuidIdArray parameter and remove JSON parsing logic
* Fix OrganizationUser_SetStatusForUsersById procedure and bump script date
* Restore OrganizationUser_SetStatusForUsersById for possible server version rollback. Add new version with the name OrganizationUser_SetStatusForUsersByGuidIdArray
* Add migration script to add stored procedure OrganizationUser_SetStatusForUsersByGuidIdArray to update user status by GUID array
---
.../Repositories/OrganizationUserRepository.cs | 4 ++--
...izationUser_SetStatusForUsersByGuidIdArray.sql | 14 ++++++++++++++
...0_AddOrgUserSetStatusForUsersByGuidIdArray.sql | 15 +++++++++++++++
3 files changed, 31 insertions(+), 2 deletions(-)
create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationUser_SetStatusForUsersByGuidIdArray.sql
create mode 100644 util/Migrator/DbScripts/2025-03-13-00_AddOrgUserSetStatusForUsersByGuidIdArray.sql
diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs
index 9b77fb216e..07b55aa44a 100644
--- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs
+++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs
@@ -563,8 +563,8 @@ public class OrganizationUserRepository : Repository, IO
await using var connection = new SqlConnection(ConnectionString);
await connection.ExecuteAsync(
- "[dbo].[OrganizationUser_SetStatusForUsersById]",
- new { OrganizationUserIds = JsonSerializer.Serialize(organizationUserIds), Status = OrganizationUserStatusType.Revoked },
+ "[dbo].[OrganizationUser_SetStatusForUsersByGuidIdArray]",
+ new { OrganizationUserIds = organizationUserIds.ToGuidIdArrayTVP(), Status = OrganizationUserStatusType.Revoked },
commandType: CommandType.StoredProcedure);
}
diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_SetStatusForUsersByGuidIdArray.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_SetStatusForUsersByGuidIdArray.sql
new file mode 100644
index 0000000000..7843748d72
--- /dev/null
+++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_SetStatusForUsersByGuidIdArray.sql
@@ -0,0 +1,14 @@
+CREATE PROCEDURE [dbo].[OrganizationUser_SetStatusForUsersByGuidIdArray]
+ @OrganizationUserIds AS [dbo].[GuidIdArray] READONLY,
+ @Status SMALLINT
+AS
+BEGIN
+ SET NOCOUNT ON
+
+ UPDATE OU
+ SET OU.[Status] = @Status
+ FROM [dbo].[OrganizationUser] OU
+ INNER JOIN @OrganizationUserIds OUI ON OUI.[Id] = OU.[Id]
+
+ EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @OrganizationUserIds
+END
diff --git a/util/Migrator/DbScripts/2025-03-13-00_AddOrgUserSetStatusForUsersByGuidIdArray.sql b/util/Migrator/DbScripts/2025-03-13-00_AddOrgUserSetStatusForUsersByGuidIdArray.sql
new file mode 100644
index 0000000000..e7c0477710
--- /dev/null
+++ b/util/Migrator/DbScripts/2025-03-13-00_AddOrgUserSetStatusForUsersByGuidIdArray.sql
@@ -0,0 +1,15 @@
+CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_SetStatusForUsersByGuidIdArray]
+ @OrganizationUserIds AS [dbo].[GuidIdArray] READONLY,
+ @Status SMALLINT
+AS
+BEGIN
+ SET NOCOUNT ON
+
+ UPDATE OU
+ SET OU.[Status] = @Status
+ FROM [dbo].[OrganizationUser] OU
+ INNER JOIN @OrganizationUserIds OUI ON OUI.[Id] = OU.[Id]
+
+ EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @OrganizationUserIds
+END
+GO
From fc827ed2097ca601c8933760993ce602373c3789 Mon Sep 17 00:00:00 2001
From: Todd Martin <106564991+trmartin4@users.noreply.github.com>
Date: Wed, 19 Mar 2025 13:49:02 -0400
Subject: [PATCH 5/9] feat(set password) [PM-17647] Add set/change password
feature flags
* Added flag values
* Added flag values
* Removed extra space
* Linting
---
src/Core/Constants.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs
index e09422871d..fb3757daab 100644
--- a/src/Core/Constants.cs
+++ b/src/Core/Constants.cs
@@ -177,6 +177,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 GetAllKeys()
{
From 21717ec71eb59057f3ac71dcce4c467f17d92bbc Mon Sep 17 00:00:00 2001
From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com>
Date: Wed, 19 Mar 2025 11:13:38 -0700
Subject: [PATCH 6/9] [PM-17733] - [Privilege Escalation] - Unauthorised access
allows limited access user to change password of Items (#5452)
* prevent view-only users from updating passwords
* revert change to licensing service
* add tests
* check if organizationId is there
* move logic to private method
* move logic to private method
* move logic into method
* revert change to licensing service
* throw exception when cipher key is created by hidden password users
* fix tests
* don't allow totp or passkeys changes from hidden password users
* add tests
* revert change to licensing service
---
.../Services/Implementations/CipherService.cs | 36 ++-
.../Vault/Services/CipherServiceTests.cs | 232 +++++++++++++++++-
2 files changed, 266 insertions(+), 2 deletions(-)
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/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(
From 481df89cf0991c02dc9ddd194de83c92cf72db14 Mon Sep 17 00:00:00 2001
From: Jason Ng
Date: Wed, 19 Mar 2025 14:24:12 -0400
Subject: [PATCH 7/9] [PM-19342] Onboarding Nudges Feature Flag (#5530)
---
src/Core/Constants.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs
index fb3757daab..970bcb4082 100644
--- a/src/Core/Constants.cs
+++ b/src/Core/Constants.cs
@@ -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";
From 3422f4cd5033c751a0479ed91e5b15820dfdd997 Mon Sep 17 00:00:00 2001
From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
Date: Wed, 19 Mar 2025 13:55:30 -0500
Subject: [PATCH 8/9] [PM-18971] Special Characters in Org Names (#5514)
* sanitize organization name for email to avoid encoding
* fix spelling mistake in variable name
---
src/Core/Services/IMailService.cs | 2 +-
.../Services/Implementations/HandlebarsMailService.cs | 9 +++++----
src/Core/Services/NoopImplementations/NoopMailService.cs | 2 +-
.../Vault/Commands/CreateManyTaskNotificationsCommand.cs | 2 +-
4 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs
index b0b884eb3e..04b302bad9 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);
}
diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs
index d96f69b0a6..588365f8c9 100644
--- a/src/Core/Services/Implementations/HandlebarsMailService.cs
+++ b/src/Core/Services/Implementations/HandlebarsMailService.cs
@@ -1201,21 +1201,22 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message);
}
- public async Task SendBulkSecurityTaskNotificationsAsync(string orgName, IEnumerable securityTaskNotificaitons)
+ public async Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable 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());
}
diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs
index 5fba545903..776dd07f19 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)
{
return Task.FromResult(0);
}
diff --git a/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs b/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs
index 58b5f65e0f..f939816301 100644
--- a/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs
+++ b/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs
@@ -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)
From db3151160a06daf0dac896d061e4dca2b982682c Mon Sep 17 00:00:00 2001
From: Patrick-Pimentel-Bitwarden
Date: Wed, 19 Mar 2025 15:27:51 -0400
Subject: [PATCH 9/9] fix(device-approval-persistence): [PM-9112] Device
Approval Persistence - Added feature flag. (#5495)
---
src/Core/Constants.cs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs
index 970bcb4082..0ae9f1d8d7 100644
--- a/src/Core/Constants.cs
+++ b/src/Core/Constants.cs
@@ -126,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";
|