mirror of
https://github.com/bitwarden/server
synced 2026-02-22 12:23:37 +00:00
[PM-22263] [PM-29849] Initial PoC of seeder API (#6424)
We want to reduce the amount of business critical test data in the company. One way of doing that is to generate test data on demand prior to client side testing. Clients will request a scene to be set up with a JSON body set of options, specific to a given scene. Successful seed requests will be responded to with a mangleMap which maps magic strings present in the request to the mangled, non-colliding versions inserted into the database. This way, the server is solely responsible for understanding uniqueness requirements in the database. scenes also are able to return custom data, depending on the scene. For example, user creation would benefit from a return value of the userId for further test setup on the client side. Clients will indicate they are running tests by including a unique header, x-play-id which specifies a unique testing context. The server uses this PlayId as the seed for any mangling that occurs. This allows the client to decide it will reuse a given PlayId if the test context builds on top of previously executed tests. When a given context is no longer needed, the API user will delete all test data associated with the PlayId by calling a delete endpoint. --------- Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
This commit is contained in:
30
src/SharedWeb/Play/PlayServiceCollectionExtensions.cs
Normal file
30
src/SharedWeb/Play/PlayServiceCollectionExtensions.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
41
src/SharedWeb/Utilities/PlayIdMiddleware.cs
Normal file
41
src/SharedWeb/Utilities/PlayIdMiddleware.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,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;
|
||||
@@ -117,6 +118,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>();
|
||||
@@ -522,6 +557,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)
|
||||
|
||||
Reference in New Issue
Block a user