From 148a6311783b2db87eab6781327570d26eec6e5e Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Fri, 31 Jan 2025 15:59:39 +0100
Subject: [PATCH 1/6] [deps]: Update github/codeql-action action to v3.28.8
(#5292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
.github/workflows/build.yml | 2 +-
.github/workflows/scan.yml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 7d64612aba..3b96eeb468 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -314,7 +314,7 @@ jobs:
output-format: sarif
- name: Upload Grype results to GitHub
- uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
+ uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
with:
sarif_file: ${{ steps.container-scan.outputs.sarif }}
diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml
index 156ebee165..ec2eb7789a 100644
--- a/.github/workflows/scan.yml
+++ b/.github/workflows/scan.yml
@@ -46,7 +46,7 @@ jobs:
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
- uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
+ uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
with:
sarif_file: cx_result.sarif
From d239170c1ca3996703c59ea3a46cf6315b925a53 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rui=20Tom=C3=A9?=
<108268980+r-tome@users.noreply.github.com>
Date: Fri, 31 Jan 2025 15:01:26 +0000
Subject: [PATCH 2/6] [PM-17697] Save Organization Name changes in Bitwarden
Portal (#5337)
* Add Org_Name_Edit permission to the Permissions enum
* Add Org_Name_Edit permission to RolePermissionMapping
* Implement Org_Name_Edit permission check in UpdateOrganization method
* Add Org_Name_Edit permission check to Organization form input
---
.../AdminConsole/Controllers/OrganizationsController.cs | 5 +++++
src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml | 3 ++-
src/Admin/Enums/Permissions.cs | 1 +
src/Admin/Utilities/RolePermissionMapping.cs | 5 +++++
4 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs
index 3fdef169b4..60a5a39612 100644
--- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs
+++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs
@@ -421,6 +421,11 @@ public class OrganizationsController : Controller
private void UpdateOrganization(Organization organization, OrganizationEditModel model)
{
+ if (_accessControlService.UserHasPermission(Permission.Org_Name_Edit))
+ {
+ organization.Name = WebUtility.HtmlEncode(model.Name);
+ }
+
if (_accessControlService.UserHasPermission(Permission.Org_CheckEnabledBox))
{
organization.Enabled = model.Enabled;
diff --git a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml
index cdc7608675..aeff65c900 100644
--- a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml
+++ b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml
@@ -12,6 +12,7 @@
var canViewBilling = AccessControlService.UserHasPermission(Permission.Org_Billing_View);
var canViewPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_View);
var canViewLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_View);
+ var canEditName = AccessControlService.UserHasPermission(Permission.Org_Name_Edit);
var canCheckEnabled = AccessControlService.UserHasPermission(Permission.Org_CheckEnabledBox);
var canEditPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_Edit);
var canEditLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_Edit);
@@ -28,7 +29,7 @@
diff --git a/src/Admin/Enums/Permissions.cs b/src/Admin/Enums/Permissions.cs
index 20c500c061..4edcd742b4 100644
--- a/src/Admin/Enums/Permissions.cs
+++ b/src/Admin/Enums/Permissions.cs
@@ -22,6 +22,7 @@ public enum Permission
Org_List_View,
Org_OrgInformation_View,
Org_GeneralDetails_View,
+ Org_Name_Edit,
Org_CheckEnabledBox,
Org_BusinessInformation_View,
Org_InitiateTrial,
diff --git a/src/Admin/Utilities/RolePermissionMapping.cs b/src/Admin/Utilities/RolePermissionMapping.cs
index 4b5a4e3802..3b510781be 100644
--- a/src/Admin/Utilities/RolePermissionMapping.cs
+++ b/src/Admin/Utilities/RolePermissionMapping.cs
@@ -24,6 +24,7 @@ public static class RolePermissionMapping
Permission.User_Billing_Edit,
Permission.User_Billing_LaunchGateway,
Permission.User_NewDeviceException_Edit,
+ Permission.Org_Name_Edit,
Permission.Org_CheckEnabledBox,
Permission.Org_List_View,
Permission.Org_OrgInformation_View,
@@ -71,6 +72,7 @@ public static class RolePermissionMapping
Permission.User_Billing_Edit,
Permission.User_Billing_LaunchGateway,
Permission.User_NewDeviceException_Edit,
+ Permission.Org_Name_Edit,
Permission.Org_CheckEnabledBox,
Permission.Org_List_View,
Permission.Org_OrgInformation_View,
@@ -116,6 +118,7 @@ public static class RolePermissionMapping
Permission.User_Billing_View,
Permission.User_Billing_LaunchGateway,
Permission.User_NewDeviceException_Edit,
+ Permission.Org_Name_Edit,
Permission.Org_CheckEnabledBox,
Permission.Org_List_View,
Permission.Org_OrgInformation_View,
@@ -148,6 +151,7 @@ public static class RolePermissionMapping
Permission.User_Billing_View,
Permission.User_Billing_Edit,
Permission.User_Billing_LaunchGateway,
+ Permission.Org_Name_Edit,
Permission.Org_CheckEnabledBox,
Permission.Org_List_View,
Permission.Org_OrgInformation_View,
@@ -185,6 +189,7 @@ public static class RolePermissionMapping
Permission.User_Premium_View,
Permission.User_Licensing_View,
Permission.User_Licensing_Edit,
+ Permission.Org_Name_Edit,
Permission.Org_CheckEnabledBox,
Permission.Org_List_View,
Permission.Org_OrgInformation_View,
From e43a8011f10d94d9ca3271fe2a21e703ce6023d1 Mon Sep 17 00:00:00 2001
From: Todd Martin <106564991+trmartin4@users.noreply.github.com>
Date: Fri, 31 Jan 2025 10:46:09 -0500
Subject: [PATCH 3/6] [PM-17709] Send New Device Login email for all new
devices (#5340)
* Send New Device Login email regardless of New Device Verification
* Adjusted tests
* Linting
* Clarified test names.
---
.../RequestValidators/DeviceValidator.cs | 34 ++++++++++---------
.../IdentityServer/DeviceValidatorTests.cs | 12 ++-----
2 files changed, 21 insertions(+), 25 deletions(-)
diff --git a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs
index 1b148c5974..fee10e10ff 100644
--- a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs
+++ b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs
@@ -85,28 +85,17 @@ public class DeviceValidator(
}
}
- // At this point we have established either new device verification is not required or the NewDeviceOtp is valid
+ // At this point we have established either new device verification is not required or the NewDeviceOtp is valid,
+ // so we save the device to the database and proceed with authentication
requestDevice.UserId = context.User.Id;
await _deviceService.SaveAsync(requestDevice);
context.Device = requestDevice;
- // backwards compatibility -- If NewDeviceVerification not enabled send the new login emails
- // PM-13340: removal Task; remove entire if block emails should no longer be sent
- if (!_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification))
+ if (!_globalSettings.DisableEmailNewDevice)
{
- // This ensures the user doesn't receive a "new device" email on the first login
- var now = DateTime.UtcNow;
- if (now - context.User.CreationDate > TimeSpan.FromMinutes(10))
- {
- var deviceType = requestDevice.Type.GetType().GetMember(requestDevice.Type.ToString())
- .FirstOrDefault()?.GetCustomAttribute()?.GetName();
- if (!_globalSettings.DisableEmailNewDevice)
- {
- await _mailService.SendNewDeviceLoggedInEmail(context.User.Email, deviceType, now,
- _currentContext.IpAddress);
- }
- }
+ await SendNewDeviceLoginEmail(context.User, requestDevice);
}
+
return true;
}
@@ -174,6 +163,19 @@ public class DeviceValidator(
return DeviceValidationResultType.NewDeviceVerificationRequired;
}
+ private async Task SendNewDeviceLoginEmail(User user, Device requestDevice)
+ {
+ // Ensure that the user doesn't receive a "new device" email on the first login
+ var now = DateTime.UtcNow;
+ if (now - user.CreationDate > TimeSpan.FromMinutes(10))
+ {
+ var deviceType = requestDevice.Type.GetType().GetMember(requestDevice.Type.ToString())
+ .FirstOrDefault()?.GetCustomAttribute()?.GetName();
+ await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now,
+ _currentContext.IpAddress);
+ }
+ }
+
public async Task GetKnownDeviceAsync(User user, Device device)
{
if (user == null || device == null)
diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs
index fa3a117c55..6e6406f16b 100644
--- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs
+++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs
@@ -227,7 +227,7 @@ public class DeviceValidatorTests
}
[Theory, BitAutoData]
- public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_SendsEmail_ReturnsTrue(
+ public async void ValidateRequestDeviceAsync_ExistingUserNewDeviceLogin_SendNewDeviceLoginEmail_ReturnsTrue(
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
@@ -237,8 +237,6 @@ public class DeviceValidatorTests
_globalSettings.DisableEmailNewDevice = false;
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
.Returns(null as Device);
- _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
- .Returns(false);
// set user creation to more than 10 minutes ago
context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11);
@@ -253,7 +251,7 @@ public class DeviceValidatorTests
}
[Theory, BitAutoData]
- public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_NewUser_DoesNotSendEmail_ReturnsTrue(
+ public async void ValidateRequestDeviceAsync_NewUserNewDeviceLogin_DoesNotSendNewDeviceLoginEmail_ReturnsTrue(
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
@@ -263,8 +261,6 @@ public class DeviceValidatorTests
_globalSettings.DisableEmailNewDevice = false;
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
.Returns(null as Device);
- _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
- .Returns(false);
// set user creation to less than 10 minutes ago
context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(9);
@@ -279,7 +275,7 @@ public class DeviceValidatorTests
}
[Theory, BitAutoData]
- public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_DisableEmailTrue_DoesNotSendEmail_ReturnsTrue(
+ public async void ValidateRequestDeviceAsynce_DisableNewDeviceLoginEmailTrue_DoesNotSendNewDeviceEmail_ReturnsTrue(
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
@@ -289,8 +285,6 @@ public class DeviceValidatorTests
_globalSettings.DisableEmailNewDevice = true;
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
.Returns(null as Device);
- _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
- .Returns(false);
// Act
var result = await _sut.ValidateRequestDeviceAsync(request, context);
From bd394eabe9c887e43893a961e191e6c0e11e1d08 Mon Sep 17 00:00:00 2001
From: Jimmy Vo
Date: Fri, 31 Jan 2025 10:50:14 -0500
Subject: [PATCH 4/6] [pm-16528] Fix entity framework query (#5333)
---
.../OrganizationDomainRepository.cs | 28 ++---
.../OrganizationDomainRepositoryTests.cs | 118 ++++++++++++++++++
2 files changed, 127 insertions(+), 19 deletions(-)
diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs
index e339c13351..50d791b81b 100644
--- a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs
+++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs
@@ -46,27 +46,17 @@ public class OrganizationDomainRepository : Repository x.VerifiedDate == null
- && x.JobRunCount != 3
- && x.NextRunDate.Year == date.Year
- && x.NextRunDate.Month == date.Month
- && x.NextRunDate.Day == date.Day
- && x.NextRunDate.Hour == date.Hour)
- .AsNoTracking()
+ var start36HoursWindow = date.AddHours(-36);
+ var end36HoursWindow = date;
+
+ var pastDomains = await dbContext.OrganizationDomains
+ .Where(x => x.NextRunDate >= start36HoursWindow
+ && x.NextRunDate <= end36HoursWindow
+ && x.VerifiedDate == null
+ && x.JobRunCount != 3)
.ToListAsync();
- //Get records that have ignored/failed by the background service
- var pastDomains = dbContext.OrganizationDomains
- .AsEnumerable()
- .Where(x => (date - x.NextRunDate).TotalHours > 36
- && x.VerifiedDate == null
- && x.JobRunCount != 3)
- .ToList();
-
- var results = domains.Union(pastDomains);
-
- return Mapper.Map>(results);
+ return Mapper.Map>(pastDomains);
}
public async Task GetOrganizationDomainSsoDetailsAsync(string email)
diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs
index 8e0b502a47..ad92f40efc 100644
--- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs
+++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs
@@ -188,4 +188,122 @@ public class OrganizationDomainRepositoryTests
var expectedDomain2 = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain2.DomainName);
Assert.Null(expectedDomain2);
}
+
+ [DatabaseTheory, DatabaseData]
+ public async Task GetManyByNextRunDateAsync_ShouldReturnUnverifiedDomains(
+ IOrganizationRepository organizationRepository,
+ IOrganizationDomainRepository organizationDomainRepository)
+ {
+ // Arrange
+ var id = Guid.NewGuid();
+
+ var organization1 = await organizationRepository.CreateAsync(new Organization
+ {
+ Name = $"Test Org {id}",
+ BillingEmail = $"test+{id}@example.com",
+ Plan = "Test",
+ PrivateKey = "privatekey",
+
+ });
+
+ var organizationDomain = new OrganizationDomain
+ {
+ OrganizationId = organization1.Id,
+ DomainName = $"domain2+{id}@example.com",
+ Txt = "btw+12345"
+ };
+
+ var within36HoursWindow = 1;
+ organizationDomain.SetNextRunDate(within36HoursWindow);
+
+ await organizationDomainRepository.CreateAsync(organizationDomain);
+
+ var date = organizationDomain.NextRunDate;
+
+ // Act
+ var domains = await organizationDomainRepository.GetManyByNextRunDateAsync(date);
+
+ // Assert
+ var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName);
+ Assert.NotNull(expectedDomain);
+ }
+
+ [DatabaseTheory, DatabaseData]
+ public async Task GetManyByNextRunDateAsync_ShouldNotReturnUnverifiedDomains_WhenNextRunDateIsOutside36hoursWindow(
+ IOrganizationRepository organizationRepository,
+ IOrganizationDomainRepository organizationDomainRepository)
+ {
+ // Arrange
+ var id = Guid.NewGuid();
+
+ var organization1 = await organizationRepository.CreateAsync(new Organization
+ {
+ Name = $"Test Org {id}",
+ BillingEmail = $"test+{id}@example.com",
+ Plan = "Test",
+ PrivateKey = "privatekey",
+
+ });
+
+ var organizationDomain = new OrganizationDomain
+ {
+ OrganizationId = organization1.Id,
+ DomainName = $"domain2+{id}@example.com",
+ Txt = "btw+12345"
+ };
+
+ var outside36HoursWindow = 20;
+ organizationDomain.SetNextRunDate(outside36HoursWindow);
+
+ await organizationDomainRepository.CreateAsync(organizationDomain);
+
+ var date = DateTimeOffset.UtcNow.Date.AddDays(1);
+
+ // Act
+ var domains = await organizationDomainRepository.GetManyByNextRunDateAsync(date);
+
+ // Assert
+ var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName);
+ Assert.Null(expectedDomain);
+ }
+
+ [DatabaseTheory, DatabaseData]
+ public async Task GetManyByNextRunDateAsync_ShouldNotReturnVerifiedDomains(
+ IOrganizationRepository organizationRepository,
+ IOrganizationDomainRepository organizationDomainRepository)
+ {
+ // Arrange
+ var id = Guid.NewGuid();
+
+ var organization1 = await organizationRepository.CreateAsync(new Organization
+ {
+ Name = $"Test Org {id}",
+ BillingEmail = $"test+{id}@example.com",
+ Plan = "Test",
+ PrivateKey = "privatekey",
+
+ });
+
+ var organizationDomain = new OrganizationDomain
+ {
+ OrganizationId = organization1.Id,
+ DomainName = $"domain2+{id}@example.com",
+ Txt = "btw+12345"
+ };
+
+ var within36HoursWindow = 1;
+ organizationDomain.SetNextRunDate(within36HoursWindow);
+ organizationDomain.SetVerifiedDate();
+
+ await organizationDomainRepository.CreateAsync(organizationDomain);
+
+ var date = DateTimeOffset.UtcNow.Date.AddDays(1);
+
+ // Act
+ var domains = await organizationDomainRepository.GetManyByNextRunDateAsync(date);
+
+ // Assert
+ var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName);
+ Assert.Null(expectedDomain);
+ }
}
From 408ddd938893448cfce270ef0754168d4d65e037 Mon Sep 17 00:00:00 2001
From: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Date: Fri, 31 Jan 2025 11:08:07 -0500
Subject: [PATCH 5/6] Scaffold Events Integration Tests (#5355)
* Scaffold Events Integration Tests
* Format
---
bitwarden-server.sln | 7 ++
.../Controllers/CollectControllerTests.cs | 29 ++++++++
.../Events.IntegrationTest.csproj | 29 ++++++++
.../EventsApplicationFactory.cs | 57 +++++++++++++++
test/Events.IntegrationTest/GlobalUsings.cs | 1 +
.../Factories/WebApplicationFactoryBase.cs | 72 ++++++++++---------
6 files changed, 163 insertions(+), 32 deletions(-)
create mode 100644 test/Events.IntegrationTest/Controllers/CollectControllerTests.cs
create mode 100644 test/Events.IntegrationTest/Events.IntegrationTest.csproj
create mode 100644 test/Events.IntegrationTest/EventsApplicationFactory.cs
create mode 100644 test/Events.IntegrationTest/GlobalUsings.cs
diff --git a/bitwarden-server.sln b/bitwarden-server.sln
index 75e7d7fade..e9aff53f8e 100644
--- a/bitwarden-server.sln
+++ b/bitwarden-server.sln
@@ -125,6 +125,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notifications.Test", "test\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.Dapper.Test", "test\Infrastructure.Dapper.Test\Infrastructure.Dapper.Test.csproj", "{4A725DB3-BE4F-4C23-9087-82D0610D67AF}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "test\Events.IntegrationTest\Events.IntegrationTest.csproj", "{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -313,6 +315,10 @@ Global
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -363,6 +369,7 @@ Global
{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{90D85D8F-5577-4570-A96E-5A2E185F0F6F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
+ {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
diff --git a/test/Events.IntegrationTest/Controllers/CollectControllerTests.cs b/test/Events.IntegrationTest/Controllers/CollectControllerTests.cs
new file mode 100644
index 0000000000..7f86758144
--- /dev/null
+++ b/test/Events.IntegrationTest/Controllers/CollectControllerTests.cs
@@ -0,0 +1,29 @@
+using System.Net.Http.Json;
+using Bit.Core.Enums;
+using Bit.Events.Models;
+
+namespace Bit.Events.IntegrationTest.Controllers;
+
+public class CollectControllerTests
+{
+ // This is a very simple test, and should be updated to assert more things, but for now
+ // it ensures that the events startup doesn't throw any errors with fairly basic configuration.
+ [Fact]
+ public async Task Post_Works()
+ {
+ var eventsApplicationFactory = new EventsApplicationFactory();
+ var (accessToken, _) = await eventsApplicationFactory.LoginWithNewAccount();
+ var client = eventsApplicationFactory.CreateAuthedClient(accessToken);
+
+ var response = await client.PostAsJsonAsync>("collect",
+ [
+ new EventModel
+ {
+ Type = EventType.User_ClientExportedVault,
+ Date = DateTime.UtcNow,
+ },
+ ]);
+
+ response.EnsureSuccessStatusCode();
+ }
+}
diff --git a/test/Events.IntegrationTest/Events.IntegrationTest.csproj b/test/Events.IntegrationTest/Events.IntegrationTest.csproj
new file mode 100644
index 0000000000..0b51185298
--- /dev/null
+++ b/test/Events.IntegrationTest/Events.IntegrationTest.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
diff --git a/test/Events.IntegrationTest/EventsApplicationFactory.cs b/test/Events.IntegrationTest/EventsApplicationFactory.cs
new file mode 100644
index 0000000000..3faf5e81bf
--- /dev/null
+++ b/test/Events.IntegrationTest/EventsApplicationFactory.cs
@@ -0,0 +1,57 @@
+using Bit.Identity.Models.Request.Accounts;
+using Bit.IntegrationTestCommon.Factories;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Data.Sqlite;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Bit.Events.IntegrationTest;
+
+public class EventsApplicationFactory : WebApplicationFactoryBase
+{
+ private readonly IdentityApplicationFactory _identityApplicationFactory;
+ private const string _connectionString = "DataSource=:memory:";
+
+ public EventsApplicationFactory()
+ {
+ SqliteConnection = new SqliteConnection(_connectionString);
+ SqliteConnection.Open();
+
+ _identityApplicationFactory = new IdentityApplicationFactory();
+ _identityApplicationFactory.SqliteConnection = SqliteConnection;
+ }
+
+ protected override void ConfigureWebHost(IWebHostBuilder builder)
+ {
+ base.ConfigureWebHost(builder);
+
+ builder.ConfigureTestServices(services =>
+ {
+ services.Configure(JwtBearerDefaults.AuthenticationScheme, options =>
+ {
+ options.BackchannelHttpHandler = _identityApplicationFactory.Server.CreateHandler();
+ });
+ });
+ }
+
+ ///
+ /// Helper for registering and logging in to a new account
+ ///
+ public async Task<(string Token, string RefreshToken)> LoginWithNewAccount(string email = "integration-test@bitwarden.com", string masterPasswordHash = "master_password_hash")
+ {
+ await _identityApplicationFactory.RegisterAsync(new RegisterRequestModel
+ {
+ Email = email,
+ MasterPasswordHash = masterPasswordHash,
+ });
+
+ return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ SqliteConnection!.Dispose();
+ }
+}
diff --git a/test/Events.IntegrationTest/GlobalUsings.cs b/test/Events.IntegrationTest/GlobalUsings.cs
new file mode 100644
index 0000000000..9df1d42179
--- /dev/null
+++ b/test/Events.IntegrationTest/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Xunit;
diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs
index d01e92ad4c..7c7f790cdc 100644
--- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs
+++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs
@@ -14,6 +14,7 @@ using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
@@ -188,44 +189,27 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory
// QUESTION: The normal licensing service should run fine on developer machines but not in CI
// should we have a fork here to leave the normal service for developers?
// TODO: Eventually add the license file to CI
- var licensingService = services.First(sd => sd.ServiceType == typeof(ILicensingService));
- services.Remove(licensingService);
- services.AddSingleton();
+ Replace(services);
// FUTURE CONSIDERATION: Add way to run this self hosted/cloud, for now it is cloud only
- var pushRegistrationService = services.First(sd => sd.ServiceType == typeof(IPushRegistrationService));
- services.Remove(pushRegistrationService);
- services.AddSingleton();
+ Replace(services);
// Even though we are cloud we currently set this up as cloud, we can use the EF/selfhosted service
// instead of using Noop for this service
// TODO: Install and use azurite in CI pipeline
- var eventWriteService = services.First(sd => sd.ServiceType == typeof(IEventWriteService));
- services.Remove(eventWriteService);
- services.AddSingleton();
+ Replace(services);
- var eventRepositoryService = services.First(sd => sd.ServiceType == typeof(IEventRepository));
- services.Remove(eventRepositoryService);
- services.AddSingleton();
+ Replace(services);
- var mailDeliveryService = services.First(sd => sd.ServiceType == typeof(IMailDeliveryService));
- services.Remove(mailDeliveryService);
- services.AddSingleton();
+ Replace(services);
- var captchaValidationService = services.First(sd => sd.ServiceType == typeof(ICaptchaValidationService));
- services.Remove(captchaValidationService);
- services.AddSingleton();
+ Replace(services);
// TODO: Install and use azurite in CI pipeline
- var installationDeviceRepository =
- services.First(sd => sd.ServiceType == typeof(IInstallationDeviceRepository));
- services.Remove(installationDeviceRepository);
- services.AddSingleton();
+ Replace(services);
// TODO: Install and use azurite in CI pipeline
- var referenceEventService = services.First(sd => sd.ServiceType == typeof(IReferenceEventService));
- services.Remove(referenceEventService);
- services.AddSingleton();
+ Replace(services);
// Our Rate limiter works so well that it begins to fail tests unless we carve out
// one whitelisted ip. We should still test the rate limiter though and they should change the Ip
@@ -245,14 +229,9 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory
services.AddSingleton();
// Noop StripePaymentService - this could be changed to integrate with our Stripe test account
- var stripePaymentService = services.First(sd => sd.ServiceType == typeof(IPaymentService));
- services.Remove(stripePaymentService);
- services.AddSingleton(Substitute.For());
+ Replace(services, Substitute.For());
- var organizationBillingService =
- services.First(sd => sd.ServiceType == typeof(IOrganizationBillingService));
- services.Remove(organizationBillingService);
- services.AddSingleton(Substitute.For());
+ Replace(services, Substitute.For());
});
foreach (var configureTestService in _configureTestServices)
@@ -261,6 +240,35 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory
}
}
+ private static void Replace(IServiceCollection services)
+ where TService : class
+ where TNewImplementation : class, TService
+ {
+ services.RemoveAll();
+ services.AddSingleton();
+ }
+
+ private static void Replace(IServiceCollection services, TService implementation)
+ where TService : class
+ {
+ services.RemoveAll();
+ services.AddSingleton(implementation);
+ }
+
+ public HttpClient CreateAuthedClient(string accessToken)
+ {
+ var handler = Server.CreateHandler((context) =>
+ {
+ context.Request.Headers.Authorization = $"Bearer {accessToken}";
+ });
+
+ return new HttpClient(handler)
+ {
+ BaseAddress = Server.BaseAddress,
+ Timeout = TimeSpan.FromSeconds(200),
+ };
+ }
+
public DatabaseContext GetDatabaseContext()
{
var scope = Services.CreateScope();
From 669c253bc62639deffd08076843873030d66e223 Mon Sep 17 00:00:00 2001
From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Date: Fri, 31 Jan 2025 12:18:10 -0600
Subject: [PATCH 6/6] chore: add limit item deletion feature flag constant,
refs PM-17214 (#5356)
---
src/Core/Constants.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs
index 6d70f0b3ce..5643ed7654 100644
--- a/src/Core/Constants.cs
+++ b/src/Core/Constants.cs
@@ -107,6 +107,7 @@ public static class FeatureFlagKeys
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
public const string IntegrationPage = "pm-14505-admin-console-integration-page";
public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications";
+ public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission";
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";