1
0
mirror of https://github.com/bitwarden/server synced 2026-03-02 19:31:24 +00:00

Merge branch 'main' into auth/pm-29584/create-email-for-emergency-access-removal

This commit is contained in:
enmande
2026-01-14 09:46:44 -05:00
185 changed files with 19973 additions and 2304 deletions

View File

@@ -0,0 +1,30 @@
using Bit.Core.Repositories;
using Bit.SharedWeb.Play.Repositories;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.SharedWeb.Play;
public static class PlayServiceCollectionExtensions
{
/// <summary>
/// Adds PlayId tracking decorators for User and Organization repositories using Dapper implementations.
/// This replaces the standard repository implementations with tracking versions
/// that record created entities for test data cleanup. Only call when TestPlayIdTrackingEnabled is true.
/// </summary>
public static void AddPlayIdTrackingDapperRepositories(this IServiceCollection services)
{
services.AddSingleton<IOrganizationRepository, DapperTestOrganizationTrackingOrganizationRepository>();
services.AddSingleton<IUserRepository, DapperTestUserTrackingUserRepository>();
}
/// <summary>
/// Adds PlayId tracking decorators for User and Organization repositories using EntityFramework implementations.
/// This replaces the standard repository implementations with tracking versions
/// that record created entities for test data cleanup. Only call when TestPlayIdTrackingEnabled is true.
/// </summary>
public static void AddPlayIdTrackingEFRepositories(this IServiceCollection services)
{
services.AddSingleton<IOrganizationRepository, EFTestOrganizationTrackingOrganizationRepository>();
services.AddSingleton<IUserRepository, EFTestUserTrackingUserRepository>();
}
}

View File

@@ -0,0 +1,32 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Infrastructure.Dapper.Repositories;
using Microsoft.Extensions.Logging;
namespace Bit.SharedWeb.Play.Repositories;
/// <summary>
/// Dapper decorator around the <see cref="Bit.Infrastructure.Dapper.Repositories.OrganizationRepository"/> that tracks
/// created Organizations for seeding.
/// </summary>
public class DapperTestOrganizationTrackingOrganizationRepository : OrganizationRepository
{
private readonly IPlayItemService _playItemService;
public DapperTestOrganizationTrackingOrganizationRepository(
IPlayItemService playItemService,
GlobalSettings globalSettings,
ILogger<OrganizationRepository> logger)
: base(globalSettings, logger)
{
_playItemService = playItemService;
}
public override async Task<Organization> CreateAsync(Organization obj)
{
var createdOrganization = await base.CreateAsync(obj);
await _playItemService.Record(createdOrganization);
return createdOrganization;
}
}

View File

@@ -0,0 +1,33 @@
using Bit.Core.Entities;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Infrastructure.Dapper.Repositories;
using Microsoft.AspNetCore.DataProtection;
namespace Bit.SharedWeb.Play.Repositories;
/// <summary>
/// Dapper decorator around the <see cref="Bit.Infrastructure.Dapper.Repositories.UserRepository"/> that tracks
/// created Users for seeding.
/// </summary>
public class DapperTestUserTrackingUserRepository : UserRepository
{
private readonly IPlayItemService _playItemService;
public DapperTestUserTrackingUserRepository(
IPlayItemService playItemService,
GlobalSettings globalSettings,
IDataProtectionProvider dataProtectionProvider)
: base(globalSettings, dataProtectionProvider)
{
_playItemService = playItemService;
}
public override async Task<User> CreateAsync(User user)
{
var createdUser = await base.CreateAsync(user);
await _playItemService.Record(createdUser);
return createdUser;
}
}

View File

@@ -0,0 +1,33 @@
using AutoMapper;
using Bit.Core.Services;
using Bit.Infrastructure.EntityFramework.Repositories;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Bit.SharedWeb.Play.Repositories;
/// <summary>
/// EntityFramework decorator around the <see cref="Bit.Infrastructure.EntityFramework.Repositories.OrganizationRepository"/> that tracks
/// created Organizations for seeding.
/// </summary>
public class EFTestOrganizationTrackingOrganizationRepository : OrganizationRepository
{
private readonly IPlayItemService _playItemService;
public EFTestOrganizationTrackingOrganizationRepository(
IPlayItemService playItemService,
IServiceScopeFactory serviceScopeFactory,
IMapper mapper,
ILogger<OrganizationRepository> logger)
: base(serviceScopeFactory, mapper, logger)
{
_playItemService = playItemService;
}
public override async Task<Core.AdminConsole.Entities.Organization> CreateAsync(Core.AdminConsole.Entities.Organization organization)
{
var createdOrganization = await base.CreateAsync(organization);
await _playItemService.Record(createdOrganization);
return createdOrganization;
}
}

View File

@@ -0,0 +1,31 @@
using AutoMapper;
using Bit.Core.Services;
using Bit.Infrastructure.EntityFramework.Repositories;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.SharedWeb.Play.Repositories;
/// <summary>
/// EntityFramework decorator around the <see cref="Bit.Infrastructure.EntityFramework.Repositories.UserRepository"/> that tracks
/// created Users for seeding.
/// </summary>
public class EFTestUserTrackingUserRepository : UserRepository
{
private readonly IPlayItemService _playItemService;
public EFTestUserTrackingUserRepository(
IPlayItemService playItemService,
IServiceScopeFactory serviceScopeFactory,
IMapper mapper)
: base(serviceScopeFactory, mapper)
{
_playItemService = playItemService;
}
public override async Task<Core.Entities.User> CreateAsync(Core.Entities.User user)
{
var createdUser = await base.CreateAsync(user);
await _playItemService.Record(createdUser);
return createdUser;
}
}

View File

@@ -1,5 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- These opt outs should be removed when all warnings are addressed -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);CA1304</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Infrastructure.Dapper\Infrastructure.Dapper.csproj" />
<ProjectReference Include="..\Core\Core.csproj" />

View File

@@ -0,0 +1,41 @@
using Bit.Core.Services;
using Microsoft.AspNetCore.Http;
namespace Bit.SharedWeb.Utilities;
/// <summary>
/// Middleware to extract the x-play-id header and set it in the PlayIdService.
///
/// PlayId is used in testing infrastructure to track data created during automated testing and fa cilitate cleanup.
/// </summary>
/// <param name="next"></param>
public sealed class PlayIdMiddleware(RequestDelegate next)
{
private const int MaxPlayIdLength = 256;
public async Task Invoke(HttpContext context, PlayIdService playIdService)
{
if (context.Request.Headers.TryGetValue("x-play-id", out var playId))
{
var playIdValue = playId.ToString();
if (string.IsNullOrWhiteSpace(playIdValue))
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsJsonAsync(new { Error = "x-play-id header cannot be empty or whitespace" });
return;
}
if (playIdValue.Length > MaxPlayIdLength)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsJsonAsync(new { Error = $"x-play-id header cannot exceed {MaxPlayIdLength} characters" });
return;
}
playIdService.PlayId = playIdValue;
}
await next(context);
}
}

View File

@@ -57,6 +57,7 @@ using Bit.Core.Vault;
using Bit.Core.Vault.Services;
using Bit.Infrastructure.Dapper;
using Bit.Infrastructure.EntityFramework;
using Bit.SharedWeb.Play;
using DnsClient;
using Duende.IdentityModel;
using LaunchDarkly.Sdk.Server;
@@ -118,6 +119,40 @@ public static class ServiceCollectionExtensions
return provider;
}
/// <summary>
/// Registers test PlayId tracking services for test data management and cleanup.
/// This infrastructure is isolated to test environments and enables tracking of test-generated entities.
/// </summary>
public static void AddTestPlayIdTracking(this IServiceCollection services, GlobalSettings globalSettings)
{
if (globalSettings.TestPlayIdTrackingEnabled)
{
var (provider, _) = GetDatabaseProvider(globalSettings);
// Include PlayIdService for tracking Play Ids in repositories
// We need the http context accessor to use the Singleton version, which pulls from the scoped version
services.AddHttpContextAccessor();
services.AddSingleton<IPlayItemService, PlayItemService>();
services.AddSingleton<IPlayIdService, PlayIdSingletonService>();
services.AddScoped<PlayIdService>();
// Replace standard repositories with PlayId tracking decorators
if (provider == SupportedDatabaseProviders.SqlServer)
{
services.AddPlayIdTrackingDapperRepositories();
}
else
{
services.AddPlayIdTrackingEFRepositories();
}
}
else
{
services.AddSingleton<IPlayIdService, NeverPlayIdServices>();
}
}
public static void AddBaseServices(this IServiceCollection services, IGlobalSettings globalSettings)
{
services.AddScoped<ICipherService, CipherService>();
@@ -523,6 +558,10 @@ public static class ServiceCollectionExtensions
IWebHostEnvironment env, GlobalSettings globalSettings)
{
app.UseMiddleware<RequestLoggingMiddleware>();
if (globalSettings.TestPlayIdTrackingEnabled)
{
app.UseMiddleware<PlayIdMiddleware>();
}
}
public static void UseForwardedHeaders(this IApplicationBuilder app, IGlobalSettings globalSettings)